Moderating Fluent Forms and Ninja Forms Submissions
Fluent Forms crossed 700,000 active installations on WordPress.org in 2026. Ninja Forms sits at 600,000 or more. Between them that is around 1.3 million sites, and yet when a developer searches for how to filter their submissions properly, almost everything that comes back is written for Contact Form 7, Gravity Forms or WPForms.
Both plugins are extensible, well documented for developers, and expose exactly the seam you need. This is where to put the check in each. If the site in front of you turns out to be on Gravity Forms instead, the equivalent hook is gform_entry_is_spam and we covered that separately.
It is worth checking which plugin you are actually looking at before you start. Fluent Forms ships a one-click migrator from Contact Form 7, WPForms, Gravity Forms and Ninja Forms, so a fair number of these installs are running forms that were originally built somewhere else, with the old plugin still sitting in the plugins list.
What You Already Have, And What It Misses
Neither plugin is defenceless. Fluent Forms ships a honeypot, a configurable minimum submission interval, and CAPTCHA fields covering reCAPTCHA, hCaptcha and Cloudflare Turnstile. Ninja Forms documents the same category of protection: Turnstile, hCaptcha, reCAPTCHA and Akismet.
Keep all of it. Then notice what the list has in common. Every item is a check on the sender. Is this a browser, is it fast enough to be a script, is this session suspicious. Not one of them opens the message field and reads it.
On the forms these plugins tend to power that is the wrong half of the problem. A quote request, a support ticket, a membership application: the risk lives in the free text, and it arrives from real humans and from language models with equal ease. We have written before about why the old tells stopped working; the short version is that fluent, unique, business-specific spam now passes every bot check on the market and looks like a lead until someone wastes half an hour on it.
Advanced and moderate bots accounted for 55% of all bot attacks in 2024, using headless browsers and anti-detection tooling to behave like ordinary visitors. In 2025 automated traffic passed 53% of all web activity and 40% of traffic was malicious. Sources: Imperva/Thales, 2025 and 2026 Bad Bot Reports
Fluent Forms: The Validation Filter
Fluent Forms exposes a per-field validation filter, fluentform/validate_input_item_{input_key}, which runs before the submission is inserted. Return an error and the submission is rejected with a normal validation message. For a textarea the key is textarea.
add_filter( 'fluentform/validate_input_item_textarea', function ( $errorMessage, $field, $formData, $fields, $form ) {
$fieldName = $field['name'];
$value = $formData[ $fieldName ] ?? '';
if ( empty( $value ) ) {
return $errorMessage;
}
$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' => $value ) ),
'timeout' => 3,
) );
if ( is_wp_error( $response ) ) {
error_log( 'Babelshield unreachable: ' . $response->get_error_message() );
return $errorMessage; // fail open
}
$scores = json_decode( wp_remote_retrieve_body( $response ), true );
error_log( 'Fluent Forms submission scored: ' . wp_json_encode( $scores ) );
if ( ( $scores['spam'] ?? 0 ) > 0.8
|| ( $scores['junk'] ?? 0 ) > 0.85
|| ( $scores['hate'] ?? 0 ) > 0.5 ) {
return array( __( 'Your message could not be processed. Please review it and try again.', 'your-textdomain' ) );
}
return $errorMessage;
}, 10, 5 );
A few things worth knowing before you ship it. The filter name changes with the field type, so a single-line text field is fluentform/validate_input_item_input_text and an email field is ..._input_email. Older code in the wild uses underscore-separated names such as fluentform_validate_input_item_input_text; the slash-separated form is the current convention. And if you would rather score several fields together, or run checks after validation has otherwise passed, fluentform/before_insert_submission fires with the prepared data just before the row is written.
Ninja Forms: The Submit Data Filter
Ninja Forms routes everything through ninja_forms_submit_data, which receives the whole submission (field data, form settings and extras) and lets you write errors back into it. Adding an entry under errors.fields keyed by field ID halts processing and surfaces the message against that field.
add_filter( 'ninja_forms_submit_data', function ( $form_data ) {
$target_form_id = 3; // your form ID
$message_field_id = 12; // your paragraph-text field ID
if ( (int) $form_data['form_id'] !== $target_form_id ) {
return $form_data;
}
$text = trim( $form_data['fields'][ $message_field_id ]['value'] ?? '' );
if ( $text === '' ) {
return $form_data;
}
$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 ) ) {
error_log( 'Babelshield unreachable: ' . $response->get_error_message() );
return $form_data; // fail open
}
$scores = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ( $scores['spam'] ?? 0 ) > 0.8 || ( $scores['hate'] ?? 0 ) > 0.5 ) {
$form_data['errors']['fields'][ $message_field_id ] =
__( 'Your message could not be processed. Please review it and try again.', 'your-textdomain' );
}
return $form_data;
}, 10, 1 );
(Endpoints, field names and thresholds are illustrative. Check the current Babelshield docs and tune to your own forms.)
Two Ninja Forms specifics. Field IDs are numeric and form-specific, so hard-coding them means the snippet breaks if someone rebuilds the form; if the site is a client's, read the field by key or loop the fields array by type instead. And always return $form_data, on every branch. A filter that returns nothing takes the submission with it.
The Rules That Apply To Both
Fail open. Every path above returns the submission unchanged when the API is unreachable. Blocking on failure turns a five-minute network incident into a day of lost enquiries you never see.
Log the scores on everything. Not just the rejections. The distribution of scores across your genuine traffic is the only honest basis for setting a threshold, and it is what lets you answer questions about a specific submission months later.
Run in observation mode first. For a week, score and log without acting. Real enquiries and junk usually separate cleanly, and the gap between the two clusters is where your threshold belongs.
Tune per form, not per site. A recruitment form receives blunt language that a corporate contact form never would. Category thresholds exist so the same integration can be strict about spam everywhere and relaxed about profanity where that is appropriate.
The Result
Nothing changes for the visitor: no CAPTCHA, no extra field, no third-party widget on the page. What changes is that both plugins stop being blind to the content of the message, which is the thing that actually causes the damage, and start making a decision you configured, on evidence you can inspect.
Two of the most-installed form plugins in WordPress deserve better than "add a honeypot and hope". Twenty lines in a snippet plugin gets them there.
References
Fluent Forms. Developer documentation: fluentform/validate_input_item_{input_key} and fluentform/before_insert_submission.
Fluent Forms. Release notes and active installation milestones, 2026.
Ninja Forms. Developer resources: submission processing hooks and ninja_forms_submit_data.
WordPress.org. Ninja Forms and Fluent Forms plugin directory listings, 2026.
Imperva/Thales. "2025 Bad Bot Report" and "2026 Bad Bot Report: Bots in the Agentic Age."