Skip to content
Guide

cf-turnstile-response invalid: Why Your Turnstile Token Is Rejected and How to Fix It

2026-06-057 min read

If your server-side validation keeps returning cf-turnstile-response invalid, you are not alone. This is one of the most common failures developers hit when integrating Cloudflare Turnstile, and the frustrating part is that the same code often works one minute and fails the next. The token your widget produced (the cf-turnstile-response value) reached your backend, you POSTed it to Cloudflare's /siteverify endpoint, and the response came back with success: false and an error code such as timeout-or-duplicate, invalid-input-response, or invalid-input-secret. On the client you may also see the generic 600010 challenge error.

The good news: an invalid cf-turnstile-response almost always traces back to a short list of concrete, fixable causes. In this guide we walk through each one, give you the exact fix, and at the end show the reliable path for automation, where you need a fresh, valid token programmatically every time.

What the cf-turnstile-response field actually is

When the Turnstile widget runs in the browser, it performs a lightweight challenge and produces a one-time token. That token is written into a hidden form input named cf-turnstile-response (and is also passed to your JavaScript callback). It is the proof that a real browser solved the challenge.

Your backend must take that token and verify it server-side by POSTing it to https://challenges.cloudflare.com/turnstile/v0/siteverify together with your secret key. Cloudflare returns JSON: { "success": true, ... } on success, or { "success": false, "error-codes": [...] } when the token is rejected. A 'cf-turnstile-response invalid' problem is really a non-success siteverify response, and the error-codes array tells you exactly why.

Root cause #1: the token is expired or already used (timeout-or-duplicate)

This is by far the most frequent reason a turnstile token is invalid. Turnstile tokens are single-use and short-lived: each token is valid for about 300 seconds (5 minutes) and can be verified exactly once. Submit it twice, or submit it after the window closes, and siteverify replies with error-codes: ["timeout-or-duplicate"].

It happens more than you'd expect in the real world: a user opens a form, gets distracted, attaches a large file, or submits over a slow network, and the token expires in flight. Or your code retries a failed request and accidentally re-verifies the same token. Or a double-clicked submit button fires two requests.

  • Refresh the widget after it expires using the JavaScript callback turnstile.reset() (or render with a fresh widget) so the user always submits a current token.
  • Verify each token exactly once. Never call /siteverify twice with the same cf-turnstile-response.
  • Disable the submit button after the first click to prevent duplicate POSTs.
  • If a form can sit open a long time, set an expired-callback that re-runs the challenge before submission.

Root cause #2: sitekey and secret mismatch (invalid-input-secret)

Your frontend renders the widget with a public sitekey; your backend verifies with a private secret key. These two must belong to the same Turnstile widget in the same Cloudflare account. If you mix a sitekey from one widget with the secret of another, or you swapped staging and production keys, every verification fails. The classic siteverify error code here is invalid-input-secret (and missing-input-secret when the secret is empty).

Fix it by opening the Cloudflare dashboard, finding the exact widget, and copying both the sitekey and the secret from that same widget. Store the secret in an environment variable and confirm it is actually loaded at runtime (a missing env var is a surprisingly common culprit). Make sure you are sending the secret as the secret form field, not the sitekey.

Root cause #3: hostname, action, or cData mismatch

A Turnstile widget is bound to the domains you configured for it. If the page that rendered the widget is on a hostname that the widget does not allow, the token will be rejected. The siteverify response includes a hostname field, plus action and cdata fields, that you can and should compare against what you expect.

Three mismatches to check. First, hostname: ensure the domain serving the widget (including subdomains and localhost during development) is on the widget's allowed list, and compare the returned hostname to your own domain. Second, action: if you set a data-action on the widget, verify the action returned by siteverify matches; treat a mismatch as a failure. Third, cData: if you pass custom data via data-cdata, validate the returned cdata. These checks also harden you against token reuse across endpoints. The client-side 600010 error frequently traces back to exactly this class of misconfiguration (domain not allowed, wrong key for the environment).

Root cause #4: dummy testing keys, idempotency, and clock skew

A few subtler causes catch experienced developers off guard.

Testing sitekeys in production: Cloudflare's dummy keys (for example the always-passes sitekey 1x00000000000000000000AA) generate a dummy token. A real production secret key will reject that dummy token, and vice versa, a test secret rejects real tokens. If you left a testing key in your build, swap it for your real widget's keys.

idempotency_key reuse: siteverify accepts an optional idempotency_key so you can safely retry verifying the same token and get a cached result. But if you reuse the same idempotency_key for a different token, you get back the cached (old) result, which looks like a wrong or invalid response. Use a fresh idempotency_key per token, or omit it.

Clock skew: token expiry is time-based, so a server clock that drifts can make fresh tokens look expired (or stretch the window incorrectly). Keep your server synced with NTP.

A quick triage checklist

Before changing code, read the error-codes array from your siteverify response. It usually names the exact problem:

  • timeout-or-duplicate -> token expired (over ~300s) or verified more than once. Get a fresh token; verify once.
  • invalid-input-response -> the cf-turnstile-response is malformed, empty, or expired in transit. Confirm the field is actually being sent.
  • invalid-input-secret / missing-input-secret -> wrong or absent secret. Re-copy the secret for the matching widget.
  • Client shows 600010 -> environment/config issue (wrong key for the domain, domain not allowed, blocked by an extension). Re-check keys and allowed hostnames.

When you need a valid token programmatically: NSLSolver

Everything above fixes a legitimate, human-facing integration. But if you are automating, running a scraper, a bot, an integration test against a third-party site, or load tooling, there is no human to solve the widget, and you cannot mint your own valid cf-turnstile-response. You need a service that drives a real challenge and returns a fresh, single-use token you can submit.

That is what NSLSolver does. You POST the target's sitekey and URL to /solve, and it returns a valid Turnstile token you use as the cf-turnstile-response value, just as a browser would. Tokens come back in roughly 250ms on average with a 99.9% success rate, and failed solves are not charged. Turnstile solving is $0.40 per 1,000 solves, pay-as-you-go, and new accounts get 100 free requests at signup, no card and no crypto required to start.

Because every token NSLSolver returns is fresh, you sidestep the single-use and expiry problems entirely, as long as you submit it promptly (within the ~300s window) and exactly once.

Here is a complete Python example that solves the Turnstile challenge and then submits the token to the target form:

solve_turnstile.py
import requests

NSL_API_KEY = "nsl_YOUR_API_KEY"
TARGET_URL = "https://target-site.com"
SITE_KEY = "0x4AAAAAAA..."  # the data-sitekey on the target page

# 1) Ask NSLSolver for a fresh, valid Turnstile token
resp = requests.post(
    "https://api.nslsolver.com/solve",
    headers={"X-API-Key": NSL_API_KEY},
    json={
        "type": "turnstile",
        "site_key": SITE_KEY,
        "url": TARGET_URL,
    },
    timeout=120,
)
resp.raise_for_status()
token = resp.json()["token"]  # e.g. "0.AAAA..."

# 2) Submit the token as the cf-turnstile-response value
# Do this promptly (within ~300s) and only once per token.
submit = requests.post(
    f"{TARGET_URL}/login",
    data={
        "username": "[email protected]",
        "password": "secret",
        "cf-turnstile-response": token,
    },
    timeout=30,
)
print(submit.status_code, submit.text[:200])

Frequently asked questions

Why does my fresh cf-turnstile-response still return timeout-or-duplicate?

Almost always because the token was verified more than once (a retry, a double-clicked submit, or two parallel requests) or it sat too long before reaching siteverify. Turnstile tokens are single-use and valid for about 300 seconds. Verify each token exactly once and submit it promptly; if a form stays open, reset the widget to mint a new token before submitting.

What is the difference between invalid-input-response and invalid-input-secret?

invalid-input-response means the token you sent (the cf-turnstile-response) is missing, malformed, or expired. invalid-input-secret means the secret key you used for verification is wrong or does not match the widget that issued the token. The first is a token problem; the second is a key-configuration problem.

How do I fix Turnstile error 600010?

600010 is a generic client-side challenge failure usually caused by a configuration or environment issue: the sitekey is wrong for the domain, the domain is not on the widget's allowed list, the wrong key is used for the environment, or a browser extension is interfering. Re-check that the sitekey and secret belong to the same widget and that your hostname is allowed, then test in a clean browser.

Can I generate a valid cf-turnstile-response token for automation?

Not by yourself, the token must come from solving a real challenge. For scrapers, bots, and automated tests you can POST the target's site_key and url to NSLSolver's /solve endpoint (type turnstile) and receive a fresh, valid token to use as the cf-turnstile-response value. Submit it once and within the 300-second window.

Stop fighting invalid Turnstile tokens

Get a fresh, valid cf-turnstile-response token on demand. 100 free requests at signup, no card required. Turnstile solving from $0.40 per 1,000, ~250ms average, and you only pay for successful solves.