WPForms Spam Protection: Where the Keyword Filter Runs Out
WPForms Lite alone reports five million or more active installations on WordPress.org, and WPForms claims more than six million sites once the paid tiers are counted. It also ships the most complete anti-spam toolkit of any mainstream WordPress form plugin: an invisible token check, Akismet, four CAPTCHA options, country filtering, allow and deny lists, and a keyword filter.
So it is a reasonable question why a well-configured WPForms site still collects junk.
The answer is in what those tools are asking. Almost every layer in that list answers one question: is this an automated submission? The single exception, the keyword filter, answers a different but equally narrow one. Does this text contain a string somebody wrote down in advance? Neither question covers the spam that most sites now receive.
Credit Where It Is Due
Before replacing anything, turn on what is already there. WPForms gives you more than most:
- Modern Anti-Spam Protection. A token, a hidden field and a timing check, running invisibly. It is enabled by default on forms created since 2024, so older forms are worth checking.
- Akismet integration, if you already have an Akismet account on the site.
- CAPTCHA options: reCAPTCHA, hCaptcha, Cloudflare Turnstile and the Custom Captcha addon.
- Filters and lists: country filtering, a keyword filter, and email or IP allow and deny lists.
- Store Spam Entries in Database, which parks blocked submissions in a Spam folder instead of destroying them.
That last setting is the one developers skip and later regret. Any filter you deploy will occasionally be wrong, and a blocked entry you can read is a problem you can fix. A blocked entry that vanished is a lost customer you never knew about.
Most of the filtering lives behind a paid licence; Lite gets the modern anti-spam layer. If you have not enabled that and the spam-entry storage, start there. The rest of this piece is about what is still arriving afterwards.
Where the Keyword Filter Runs Out
The keyword filter is exactly what it sounds like: a list you maintain, matched case-insensitively against the fields of a submission. It is the only part of the stack that looks at content at all, which makes its limits worth understanding properly.
Modern spam has no fixed strings. A language model generates a different message every time, fluent and specific to your business. There is no repeated phrase to add to a list, which is the whole premise the filter runs on.
Substring matching cuts both ways. The filter matches anywhere in the text, so blocking cialis silently rejects every enquiry from a specialist. That is the Scunthorpe problem in miniature, and it costs you real leads without ever announcing itself.
The list is monolingual. A Portuguese or Turkish version of the same pitch is unfamiliar text, and unfamiliar is not the same as flagged, as we covered when looking at multilingual abuse.
Somebody has to own it. Every new spam wave means another round of reading junk, extracting phrases, and adding them. That work never finishes, and it is invisible on an invoice.
Underneath all four is a granularity problem. A keyword hit tells you a word appeared. It does not tell you whether the submission is spam, and those are very different facts.
Advanced and moderate bots made up 55% of all bot attacks in 2024, using headless browsers, residential proxies and anti-detection tooling to look like ordinary visitors. By 2025 automated traffic had reached more than 53% of all web activity, with 40% of traffic classified as malicious. Sources: Imperva/Thales, 2025 and 2026 Bad Bot Reports
Score the Submission Instead
The alternative is to evaluate what was typed. Send the free-text fields to a moderation API on submit, read back category scores for spam, profanity, hate speech and junk, and act on thresholds you set.
WPForms gives you a clean place to do it. The wpforms_process action fires after fields have been validated and sanitised, and it is explicitly the hook for checks that may need to raise an error and halt processing.
add_action( 'wpforms_process', function ( $fields, $entry, $form_data ) {
// Score the free-text fields, where the real signal lives.
$text = '';
foreach ( $fields as $field ) {
if ( in_array( $field['type'], array( 'textarea', 'text' ), true ) ) {
$text .= ' ' . $field['value'];
}
}
$text = trim( $text );
if ( $text === '' ) {
return;
}
$response = wp_remote_post( 'https://api.babelshield.com/v1/score', array(
'headers' => array(
'Authorization' => 'Bearer ' . BABELSHIELD_API_KEY,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array( 'text' => $text ) ),
'timeout' => 3,
) );
if ( is_wp_error( $response ) ) {
// Fail open: a moderation outage must never eat a real enquiry.
error_log( 'Babelshield unreachable: ' . $response->get_error_message() );
return;
}
$scores = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ( $scores['spam'] ?? 0 ) > 0.8
|| ( $scores['junk'] ?? 0 ) > 0.85
|| ( $scores['hate'] ?? 0 ) > 0.5 ) {
wpforms()->process->errors[ $form_data['id'] ]['header'] = esc_html__(
'Your message could not be processed. Please review it and try again.',
'your-textdomain'
);
}
}, 10, 3 );
(Endpoint, field names and thresholds are illustrative. Check the current Babelshield docs and tune to your own forms.)
Setting an error on the form ID stops the entry being saved and stops the notifications firing, which is the behaviour you want: nothing reaches the database, the inbox, or whatever the form feeds.
Three details in that snippet matter more than the API call itself. The timeout is short, so a slow response never leaves a visitor staring at a spinner. The error path returns without blocking, because an unreachable service should degrade to "no moderation", not to "no submissions". And the thresholds are separate per category, so a support form where customers legitimately swear can run loose on profanity while staying strict on spam.
If you would rather score one specific field than concatenate several, wpforms_process_validate_textarea and its siblings run earlier, per field type, and take the same shape.
Score First, Decide Second
Blocking is not the only option, and on a lead form it is rarely the best one.
Start in log-only mode. Run the scoring for a week without acting on it, write the scores to your log or a hidden entry field, then look at where your genuine submissions sit against the junk. On most contact forms the two clusters are nowhere near each other, which leaves a wide band to draw the line in.
From there you have choices. Hard-block the obvious cases. Route mid-range scores to the Spam folder for a daily glance rather than rejecting them outright. Or let everything through and use the score to prioritise, so the cleanest, highest-intent enquiries go to the top of the queue. All three are better than a wordlist, and the third adds no risk at all.
This is the same shape as the Gravity Forms approach we covered earlier, where gform_entry_is_spam routes a flagged entry to the spam folder rather than deleting it. Both plugins are safe to run this on precisely because a false positive stays recoverable.
What Changes
The visitor experience does not change: no puzzle, no extra step, no third-party widget loading on the page. What changes is what reaches you. The AI-written pitch that reads like a real enquiry, the abuse aimed at whoever reads the queue, the promotional garbage in the message field from a perfectly human sender. All of it gets judged on what it actually says.
WPForms already handles the bots better than most. Give it something that handles the content, and stop maintaining a list that was never going to keep up.
References
WordPress.org. WPForms plugin directory listing and installation statistics, 2026.
WPForms. Developer documentation for the wpforms_process action and wpforms_process_validate_{$field_type}.
WPForms. Spam Protection and Security documentation: modern anti-spam, filtering, and spam entry storage.
Imperva/Thales. "2025 Bad Bot Report" and "2026 Bad Bot Report: Bots in the Agentic Age."