Required request fields and response checks
Send the secret key and the response token in a POST request to Cloudflare's Siteverify endpoint. The remote IP and an idempotency key are optional fields for workflows that need them.
At minimum, require success:true. In a production integration, also compare hostname and action when they are part of your widget configuration, and log error codes at a level that helps operators without recording secrets or full personal form content.
- ▸ secret: server-side key
- ▸ response: token received from the browser
- ▸ remoteip: optional visitor address
- ▸ idempotency_key: optional UUID for retry protection
Node.js example
Keep validation in a small function with a timeout and a narrow return type. The caller should treat exceptions and non-success responses as rejection, then decide how much detail is safe to show the user.
export async function verifyTurnstile(token: string, remoteIp?: string) {
const body = new URLSearchParams({
secret: process.env.TURNSTILE_SECRET_KEY!,
response: token,
});
if (remoteIp) body.set("remoteip", remoteIp);
const response = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{ method: "POST", body, signal: AbortSignal.timeout(5000) }
);
if (!response.ok) return { success: false, reason: "upstream" };
const result = await response.json();
return { success: result.success === true, result };
}
Python example
The same pattern applies in Flask, Django or FastAPI: validate before the protected work, set a bounded timeout and reject malformed upstream responses.
import os
import requests
def verify_turnstile(token, remote_ip=None):
payload = {
"secret": os.environ["TURNSTILE_SECRET_KEY"],
"response": token,
}
if remote_ip:
payload["remoteip"] = remote_ip
response = requests.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data=payload,
timeout=5,
)
response.raise_for_status()
result = response.json()
return result.get("success") is True, result
Token expiry and replay are expected states
Tokens expire after 300 seconds and are single-use. A timeout-or-duplicate response can mean the user waited too long, the browser sent the same token twice, or the application retried after Siteverify had already consumed it.
Return a retryable message and reset the client widget. Do not loop the same token back to Siteverify. For payments or account creation, use a separate application idempotency key so a fresh Turnstile token does not repeat the business action.
Define the upstream failure policy
If Siteverify times out or returns an invalid body, high-risk endpoints should fail closed. A low-risk newsletter form might choose a queue or controlled retry, but that is a business decision and should not happen accidentally because an exception handler defaults to success.
Use short timeouts, track upstream failure rate and distinguish verification service failures from invalid visitor tokens.
Log enough to debug, not enough to leak
Record the endpoint, expected action, result, error codes, request correlation ID and coarse timing. Do not log the secret key. Avoid storing the full response token because it is unnecessary and can turn logs into a credential repository.
If remote IP is used, apply the same privacy and retention rules as the rest of the application logs.
- ▸ Never include the secret in client code
- ▸ Redact tokens from request logging
- ▸ Monitor spikes in timeout or invalid-input errors
- ▸ Separate user-facing messages from operator diagnostics