Skip to content

Knowledgebase

Adding Cloudflare Turnstile with mandatory server-side verification

performance, security-ssl

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.

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

  1. Create a Turnstile widget in Cloudflare with the correct allowed hostnames and desired display mode.
  2. Install the supported integration or add the client widget so submissions include its response token.
  3. In the backend, send the token and secret to the Siteverify endpoint over HTTPS before performing the protected action.
  4. 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.
  5. 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.

Code or configuration example
$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.

Was this answer helpful?

Need a little more help?

Our support team can help you find the next step.

Contact support