Validate the token·续
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)}"); } }
---
## Advanced validation techniques
*Idempotency keys for retry operationjs*
```js
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"] };
}
}
}
}
Enhanced validation with custom checksjs
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,
};
}
// Usage
const 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);
}
API response format
Examplejson
{
"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"
}
}
Examplejson
{
"success": false,
"error-codes": ["invalid-input-response"]
}
Response fields
| Field | Description |
|---|---|
success |
Boolean indicating if validation was successful |
challenge_ts |
ISO 8601 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 codes reference
| 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 |
Implementation
Example implementationjs
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);
}
}
}
// Usage
const 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);
}
Testing
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.
Best practices
Security
- 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.
- Only call the Siteverify API in your backend environment. If you expose the secret key in the front-end client code to call Siteverify, attackers can bypass the security check. Ensure that your client-side code sends the validation token to your backend, and that your backend is the sole caller of the Siteverify API.
Performance
- 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.
Error handling
- 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?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#page","headline":"Validate the token","description":"Validate Turnstile tokens on your server with the siteverify API.","url":"https://developers.cloudflare.com/turnstile/get-started/server-side-validation/","inLanguage":"en","image":"https://developers.cloudflare.com/turnstile/get-started/server-side-validation/og.png?v=58220cfac6c19059","dateModified":"2026-10-22","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["REST API"]}
description: Create and manage Turnstile widgets using the Cloudflare API. title: Create and manage widgets using Cloudflare API image: https://developers.cloudflare.com/turnstile/get-started/widget-management/api/og.png?v=3ee3163dce753625
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/turnstile/llms.txt
Use this file to discover all available pages before exploring further.
Docs