Turnstile can protect a form from unwanted automated submissions, but the server must validate each token before accepting the operation. Displaying a successful widget alone does not protect a backend endpoint.
IN THIS ARTICLE
Check access and requirements
Choose a maintained integration for the application or prepare to update its server-side handler. Keep the sitekey public, store the secret securely on the server and retain normal authentication, CSRF and input validation.
01 Add Cloudflare Turnstile with mandatory server-side verification
- Create a Turnstile widget in Cloudflare with the correct allowed hostnames and desired display mode.
- Install the supported integration or add the client widget so submissions include its response token.
- In the backend, send the token and secret to the Siteverify endpoint over HTTPS before performing the protected action.
- Require a successful validation and check expected hostname and action information where your integration uses them. Handle expiry, replay and verification failures with a useful retry flow.
- Test valid submission, missing token, invalid token and replay attempts. Use official test keys in a development environment rather than weakening production validation.
02 Example: validate Turnstile before accepting a PHP contact form
This verification stage assumes the form is served on example.com, the widget action is contact, and PHP cURL is available. Supply the secret through protected configuration and retain the form’s normal CSRF, input and rate-limit checks.
$secret = getenv('TURNSTILE_SECRET_KEY');
$token = $_POST['cf-turnstile-response'] ?? '';
if (!is_string($secret) || $secret === '') {
http_response_code(503);
exit;
}
if (!is_string($token) || $token === '') {
http_response_code(400);
exit;
}
$request = curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify');
curl_setopt_array($request, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['secret' => $secret, 'response' => $token]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 8,
]);
$body = curl_exec($request);
$status = curl_getinfo($request, CURLINFO_HTTP_CODE);
curl_close($request);
if ($body === false || $status !== 200) {
http_response_code(503);
exit;
}
$result = json_decode($body, true);
if (!is_array($result) || ($result['success'] ?? false) !== true
|| ($result['hostname'] ?? '') !== 'example.com'
|| ($result['action'] ?? '') !== 'contact') {
http_response_code(400);
exit;
}
Only continue with the protected action after this stage succeeds. Change the expected hostname and action to the actual form values. A missing secret or verification outage must not become unconditional acceptance.
Expected result: Valid test submissions pass, while missing, modified, expired or replayed tokens fail before mail or other business work is performed.
Confirm the expected result
The server should reject a direct request that bypasses the widget, while legitimate visitors can complete the form. Avoid processing payments, creating accounts or sending messages before validation succeeds.
Resolve setup problems
Tokens are single-use and expire after five minutes, so long-lived forms may need a refreshed token. Never expose the secret in page source, logs or client JavaScript. A network validation failure must not silently become unconditional acceptance.
Does Turnstile replace CSRF protection?
No. It addresses a different part of request abuse. Keep application authorisation, CSRF checks, server-side validation and appropriate rate limits alongside it.