Add Turnstile to a custom form
Updated September 13, 2026
Adding Turnstile to a form the plugin does not know about takes two pieces: render the widget where you want it, and verify the token when the form is submitted. Both have a single helper function.
Render the widget
Hook whatever your form offers as a “before the submit button” point and call the render helper:
add_action( 'my_plugin_before_submit_button', function () {
cfturnstile_field_show(
'.my-form-submit', // submit button selector
'', // JS callback, optional
'my-plugin-form', // form name, used as the analytics label
'-my-plugin-' . wp_rand() // unique id suffix
);
} );
The helper handles whitelisting, the disable filter, failsafe rendering and script enqueuing for you. The unique id suffix must genuinely be unique on the page — duplicate DOM ids mean Cloudflare renders into the first match and leaves the rest blank.
If you output the widget markup by hand rather than using the helper, fire do_action( 'cfturnstile_enqueue_scripts' ); so the Cloudflare script and plugin styles are loaded.
Verify the token
Hook your form’s validation point and check the result:
add_action( 'my_plugin_validate_submission', function ( $errors ) {
$check = cfturnstile_check( '', 'my-plugin-form' );
if ( empty( $check['success'] ) ) {
$errors->add( 'turnstile', cfturnstile_failed_message() );
}
return $errors;
} );
Passing an empty first argument lets the helper read the token from $_POST['cf-turnstile-response'], which is where the widget puts it. Use cfturnstile_failed_message() for the error text so your form respects the site’s configured message.
Login and registration forms
If your form logs a user in or registers one, the global WordPress checks will also fire on the same request. They hook authenticate and registration_errors globally, and because tokens are single-use, whichever check runs first spends the token and the second fails.
Return true from the relevant filter while your integration is handling the request:
add_filter( 'cfturnstile_wp_login_checks', '__return_true' );
Both filters use a strict comparison, so return the boolean rather than a truthy value. Scope it as tightly as you can — ideally only when you can see your own form’s fields in the request.
Forms that submit without a page load
For AJAX or single-page forms, the plugin resets the widget shortly after submit so a retry gets a fresh token. If your form resubmits itself after asynchronous work, exclude it via cfturnstile_token_refresh_skip_forms, otherwise the reset can swap the token out mid-flight.
Conventions if you are contributing one
Integrations live in inc/integrations/<category>/<plugin>.php, wrapped in a check for their enabling option. Register any new option in the settings allowlist as well as adding the UI — a field the UI shows but the allowlist omits gets wiped on every save.


