Validate the token
Learn how to securely validate Turnstile tokens on your server using the Siteverify API.
- 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
success
orfailure
and additional data. - Server takes action: Allow or reject the original request based on validation.
POST https://challenges.cloudflare.com/turnstile/v0/siteverify
The API accepts both application/x-www-form-urlencoded
and application/json
requests, but always returns JSON responses.
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 | UUID for retry protection |
- 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
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 }); } }
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'] }; }}
<?phpfunction validateTurnstile($token, $secret, $remoteip = null) { $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
$data = [ 'secret' => $secret, 'response' => $token ];
if ($remoteip) { $data['remoteip'] = $remoteip; }
$options = [ 'http' => [ 'header' => "Content-type: application/x-www-form-urlencoded\r\n", 'method' => 'POST', 'content' => http_build_query($data) ] ];
$context = stream_context_create($options); $response = file_get_contents($url, false, $context);
if ($response === FALSE) { 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 Flaskfrom 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;
@Servicepublic 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()); }}
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();
var validation = await _turnstileService.ValidateTokenAsync(cfTurnstileResponse, remoteip);
if (validation.Success) { // Valid token - process form return Ok("Form submitted successfully"); } else { // Invalid token - reject submission return BadRequest($"Verification failed: {string.Join(", ", validation.ErrorCodes)}"); }}
const crypto = require('crypto');
async function validateWithRetry(token, remoteip, maxRetries = 3) { const idempotencyKey = crypto.randomUUID();
for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const formData = new FormData(); formData.append('secret', SECRET_KEY); formData.append('response', token); formData.append('remoteip', remoteip); formData.append('idempotency_key', idempotencyKey);
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: formData });
const result = await response.json();
if (response.ok) { return result; }
// If this is the last attempt, return the error if (attempt === maxRetries) { return result; }
// Wait before retrying (exponential backoff) await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
} catch (error) { if (attempt === maxRetries) { return { success: false, 'error-codes': ['internal-error'] }; } } }}
async function validateTurnstileEnhanced(token, remoteip, expectedAction = null, expectedHostname = null) { const validation = await validateTurnstile(token, remoteip);
if (!validation.success) { return { valid: false, reason: 'turnstile_failed', errors: validation['error-codes'] }; }
// Check if action matches expected value (if specified) if (expectedAction && validation.action !== expectedAction) { return { valid: false, reason: 'action_mismatch', expected: expectedAction, received: validation.action }; }
// Check if hostname matches expected value (if specified) if (expectedHostname && validation.hostname !== expectedHostname) { return { valid: false, reason: 'hostname_mismatch', expected: expectedHostname, received: validation.hostname }; }
// Check token age (warn if older than 4 minutes) const challengeTime = new Date(validation.challenge_ts); const now = new Date(); const ageMinutes = (now - challengeTime) / (1000 * 60);
if (ageMinutes > 4) { console.warn(`Token is ${ageMinutes.toFixed(1)} minutes old`); }
return { valid: true, data: validation, tokenAge: ageMinutes };}
// Usageconst result = await validateTurnstileEnhanced( token, remoteip, 'login', // expected action 'example.com' // expected hostname);
if (result.valid) { // Process the request console.log('Validation successful:', result.data);} else { // Handle validation failure console.log('Validation failed:', result.reason);}
{ "success": true, "challenge_ts": "2022-02-28T15:14:30.096Z", "hostname": "example.com", "error-codes": [], "action": "login", "cdata": "sessionid-123456789", "metadata": { "ephemeral_id": "x:9f78e0ed210960d7693b167e" }}
{ "success": false, "error-codes": ["invalid-input-response"]}
Field | Description |
---|---|
success | Boolean indicating if validation was successful |
challenge_ts | ISO timestamp when the challenge was solved |
hostname | Hostname where the challenge was served |
error-codes | Array of error codes (if validation failed) |
action | Custom action identifier from client-side |
cdata | Custom data payload from client-side |
metadata.ephemeral_id | Device fingerprint ID (Enterprise only) |
Error code | Description | Action required |
---|---|---|
missing-input-secret | Secret parameter not provided | Ensure secret key is included |
invalid-input-secret | Secret key is invalid or expired | Check your secret key in the Cloudflare dashboard |
missing-input-response | Response parameter was not provided | Ensure token is included |
invalid-input-response | Token is invalid, malformed, or expired | User should retry the challenge |
bad-request | Request is malformed | Check request format and parameters |
timeout-or-duplicate | Token has already been validated | Each token can only be used once |
internal-error | Internal error occurred | Retry the request |
class TurnstileValidator { constructor(secretKey, timeout = 10000) { this.secretKey = secretKey; this.timeout = timeout; }
async validate(token, remoteip, options = {}) { // Input validation if (!token || typeof token !== 'string') { return { success: false, error: 'Invalid token format' }; }
if (token.length > 2048) { return { success: false, error: 'Token too long' }; }
// Prepare request const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try { const formData = new FormData(); formData.append('secret', this.secretKey); formData.append('response', token);
if (remoteip) { formData.append('remoteip', remoteip); }
if (options.idempotencyKey) { formData.append('idempotency_key', options.idempotencyKey); }
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: formData, signal: controller.signal });
const result = await response.json();
// Additional validation if (result.success) { if (options.expectedAction && result.action !== options.expectedAction) { return { success: false, error: 'Action mismatch', expected: options.expectedAction, received: result.action }; }
if (options.expectedHostname && result.hostname !== options.expectedHostname) { return { success: false, error: 'Hostname mismatch', expected: options.expectedHostname, received: result.hostname }; } }
return result;
} catch (error) { if (error.name === 'AbortError') { return { success: false, error: 'Validation timeout' }; }
console.error('Turnstile validation error:', error); return { success: false, error: 'Internal error' };
} finally { clearTimeout(timeoutId); } }}
// Usageconst validator = new TurnstileValidator(process.env.TURNSTILE_SECRET_KEY);
const result = await validator.validate(token, remoteip, { expectedAction: 'login', expectedHostname: 'example.com'});
if (result.success) { // Process the request} else { // Handle failure console.log('Validation failed:', result.error);}
You can test the dummy token generated with testing sitekey via Siteverify API with the testing secret key. Your production secret keys will reject dummy tokens.
Refer to Testing for more information.
- Store your secret keys securely. Use environment variables or secure key management.
- Validate the token on every request. Never trust client-side validation alone.
- Check additional fields. Validate the action and hostname when specified.
- Monitor for abuse and log failed validations and unusual patterns.
- Use HTTPS. Always validate over secure connections.
- Set reasonable timeouts. Do not wait indefinitely for Siteverify responses.
- Implement retry logic and handle temporary network issues.
- Cache validation results for the same token, if it is needed for your flow.
- Monitor your API latency. Track the Siteverify response time.
- Have fallback behavior for API failures.
- Use user-friendly messaging. Do not expose internal error details to users.
- Properly log errors for debugging without exposing secrets.
- Rate limit to protect against validation flooding.
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Products
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark