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-duplicateerror code.
Process
- Client generates token: Visitor completes Turnstile challenge on your webpage.
- Token sent to server: Form submission includes the Turnstile token.
- Server validates token: Your server calls Cloudflare's Siteverify API.
- Cloudflare responds: Returns
successorfailureand additional data. - 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();
