Validate the token

Last updated Oct 22, 2026 Learn how to securely validate Turnstile tokens on your server using the Siteverify API.

Mandatory server-side validation

You must call the Siteverify API to complete your Turnstile implementation. The client-side widget alone does not protect your forms.

Server-side validation is required because:

  • Tokens can be forged. An attacker can submit any string to your form endpoint without completing a challenge.
  • Tokens expire. Each token is valid for 300 seconds (5 minutes) after generation.
  • Tokens are single-use. Each token can only be validated once. A replayed token will be rejected with the timeout-or-duplicate error code.

Process

  1. Client generates token: Visitor completes Turnstile challenge on your webpage.
  2. Token sent to server: Form submission includes the Turnstile token.
  3. Server validates token: Your server calls Cloudflare's Siteverify API.
  4. Cloudflare responds: Returns success or failure and additional data.
  5. Server takes action: Allow or reject the original request based on validation.

Siteverify API overview

Endpointshell

POST https://challenges.cloudflare.com/turnstile/v0/siteverify

Request format

The API accepts both application/x-www-form-urlencoded and application/json requests, but always returns JSON responses.

Required parameters

Parameter Required Description
secret Yes Your widget's secret key from the Cloudflare dashboard
response Yes The token from the client-side widget
remoteip No The visitor's IP address
idempotency_key No A UUID you generate to safely retry validation requests

Token characteristics

  • Maximum length: 2048 characters
  • Validity period: 300 seconds (5 minutes) from generation
  • Single use: Each token can only be validated once
  • Automatic expiry: Tokens automatically expire and cannot be reused

The validation token issued by Turnstile is valid for five minutes. If a user submits the form after this period, the token is considered expired. In this scenario, the server-side verification API will return a failure, and the error-codes field in the response will include timeout-or-duplicate.

To ensure a successful validation, the visitor must initiate the request and submit the token to your backend within the five-minute window. Otherwise, the Turnstile widget needs to be refreshed to generate a new token. This can be done using the turnstile.reset function.


Basic validation examples

JSON

const SECRET_KEY = "your-secret-key";

async function validateTurnstile(token, remoteip) {
    try {
        const response = await fetch(
            "https://challenges.cloudflare.com/turnstile/v0/siteverify",
            {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    secret: SECRET_KEY,
                    response: token,
                    remoteip: remoteip,
                }),
            },
        );

const result = await response.json();
        return result;
    } catch (error) {
        console.error("Turnstile validation error:", error);
        return { success: false, "error-codes": ["internal-error"] };
    }
}

Form Data

const SECRET_KEY = "your-secret-key";

async function validateTurnstile(token, remoteip) {
    const formData = new FormData();
    formData.append("secret", SECRET_KEY);
    formData.append("response", token);
    formData.append("remoteip", remoteip);

try {
        const response = await fetch(
            "https://challenges.cloudflare.com/turnstile/v0/siteverify",
            {
                method: "POST",
                body: formData,
            },
        );

const result = await response.json();
        return result;
    } catch (error) {
        console.error("Turnstile validation error:", error);
        return { success: false, "error-codes": ["internal-error"] };
    }
}

// Usage in form handler
async function handleFormSubmission(request) {
    const body = await request.formData();
    const token = body.get("cf-turnstile-response");
    const ip =
        request.headers.get("CF-Connecting-IP") ||
        request.headers.get("X-Forwarded-For") ||
        "unknown";

const validation = await validateTurnstile(token, ip);

if (validation.success) {
        // Token is valid - process the form
        console.log("Valid submission from:", validation.hostname);
        return processForm(body);
    } else {
        // Token is invalid - reject the submission
        console.log("Invalid token:", validation["error-codes"]);
        return new Response("Invalid verification", { status: 400 });
    }
}
<?php
function validateTurnstile($token, $secret, $remoteip = null) {
    $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';

$data = [
        'secret' => $secret,
        'response' => $token
    ];

if ($remoteip) {
        $data['remoteip'] = $remoteip;
    }

$ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($data),
        CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 10,
    ]);

$response = curl_exec($ch);
    $error = curl_error($ch);
    curl_close($ch);

if ($response === false || $error !== '') {
        return ['success' => false, 'error-codes' => ['internal-error']];
    }

return json_decode($response, true);

}

// Usage
$secret_key = 'your-secret-key';
$token = $_POST['cf-turnstile-response'] ?? '';
$remoteip = $_SERVER['HTTP_CF_CONNECTING_IP'] ??
$_SERVER['HTTP_X_FORWARDED_FOR'] ??
$_SERVER['REMOTE_ADDR'];

$validation = validateTurnstile($token, $secret_key, $remoteip);

if ($validation['success']) {
// Valid token - process form
echo "Form submission successful!";
// Process your form data here
} else {
// Invalid token - show error
echo "Verification failed. Please try again.";
error_log('Turnstile validation failed: ' . implode(', ', $validation['error-codes']));
}
?>
import requests

def validate_turnstile(token, secret, remoteip=None):
    url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'

data = {
        'secret': secret,
        'response': token
    }

if remoteip:
        data['remoteip'] = remoteip

try:
        response = requests.post(url, data=data, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"Turnstile validation error: {e}")
        return {'success': False, 'error-codes': ['internal-error']}

# Usage with Flask
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET_KEY = 'your-secret-key'

@app.route('/submit-form', methods=['POST'])
def submit_form():
    token = request.form.get('cf-turnstile-response')
    remoteip = request.headers.get('CF-Connecting-IP') or \
               request.headers.get('X-Forwarded-For') or \
               request.remote_addr

validation = validate_turnstile(token, SECRET_KEY, remoteip)

if validation['success']:
        # Valid token - process form
        return jsonify({'status': 'success', 'message': 'Form submitted successfully'})
    else:
        # Invalid token - reject submission
        return jsonify({
            'status': 'error',
            'message': 'Verification failed',
            'errors': validation['error-codes']
        }), 400
import org.springframework.web.client.RestTemplate;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

@Service
public class TurnstileService {
private static final String SITEVERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
private final String secretKey = "your-secret-key";
private final RestTemplate restTemplate = new RestTemplate();

public TurnstileResponse validateToken(String token, String remoteip) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("secret", secretKey);
        params.add("response", token);
        if (remoteip != null) {
            params.add("remoteip", remoteip);
        }

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

try {
            ResponseEntity<TurnstileResponse> response = restTemplate.postForEntity(
                SITEVERIFY_URL, request, TurnstileResponse.class);
            return response.getBody();
        } catch (Exception e) {
            TurnstileResponse errorResponse = new TurnstileResponse();
            errorResponse.setSuccess(false);
            errorResponse.setErrorCodes(List.of("internal-error"));
            return errorResponse;
        }
    }

}

// Controller usage
@PostMapping("/submit-form")
public ResponseEntity<?> submitForm(
@RequestParam("cf-turnstile-response") String token,
HttpServletRequest request) {

String remoteip = request.getHeader("CF-Connecting-IP");
    if (remoteip == null) {
        remoteip = request.getHeader("X-Forwarded-For");
    }
    if (remoteip == null) {
        remoteip = request.getRemoteAddr();
    }

TurnstileResponse validation = turnstileService.validateToken(token, remoteip);

if (validation.isSuccess()) {
        // Valid token - process form
        return ResponseEntity.ok("Form submitted successfully");
    } else {
        // Invalid token - reject submission
        return ResponseEntity.badRequest()
            .body("Verification failed: " + validation.getErrorCodes());
    }

}

```csharp using System.Text.Json;

public class TurnstileService { private readonly HttpClient _httpClient; private readonly string _secretKey = "your-secret-key"; private const string SiteverifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify";

public TurnstileService(HttpClient httpClient) { _httpClient = httpClient; }

public async Task<TurnstileResponse> ValidateTokenAsync(string token, string remoteip = null) { var parameters = new Dictionary<string, string> { { "secret", _secretKey }, { "response", token } };

if (!string.IsNullOrEmpty(remoteip)) { parameters.Add("remoteip", remoteip); }

var postContent = new FormUrlEncodedContent(parameters);

try { var response = await _httpClient.PostAsync(SiteverifyUrl, postContent); var stringContent = await response.Content.ReadAsStringAsync();

return JsonSerializer.Deserialize<TurnstileResponse>(stringContent); } catch (Exception ex) { return new TurnstileResponse { Success = false, ErrorCodes = new[] { "internal-error" } }; } } }

// Controller usage [HttpPost("submit-form")] public async Task<IActionResult> SubmitForm([FromForm] string cfTurnstileResponse) { var remoteip = HttpContext.Request.Headers["CF-Connecting-IP"].FirstOrDefault() ?? HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault() ?? HttpContext.Connection.RemoteIpAddress?.ToString();