glossary / turnstile test keys

Cloudflare Turnstile test sitekey and secret key reference

A Cloudflare Turnstile test sitekey is one of five documented dummy sitekeys that make the widget behave predictably instead of running a real challenge: it always passes, always fails, or forces an interactive challenge, on any domain including localhost. Each test sitekey emits the dummy token XXXX.DUMMY.TOKEN.XXXX, which only a matching dummy secret key will accept — production secret keys reject it, and dummy secret keys reject real tokens.

The official test keys

Cloudflare publishes five dummy sitekeys and three dummy secret keys. They are not secrets, they are not tied to your account, and they work on any hostname — localhost, 127.0.0.1, 0.0.0.0, or any development domain. Real sitekeys are scoped to the hostnames you configure; dummy ones are not.

SitekeyBehaviourWidget typeUse case
1x00000000000000000000AAAlways passesVisibleTest successful form submissions
2x00000000000000000000ABAlways failsVisibleTest error handling and retry logic
1x00000000000000000000BBAlways passesInvisibleTest invisible widget success flows
2x00000000000000000000BBAlways failsInvisibleTest invisible widget error handling
3x00000000000000000000FFForces interactive challengeVisibleTest user interaction scenarios

The secret keys go on your server, in the call to the siteverify API.

Secret keyBehaviourUse case
1x0000000000000000000000000000000AAAlways passes validationTest successful token validation
2x0000000000000000000000000000000AAAlways fails validationTest validation error handling
3x0000000000000000000000000000000AAReturns "token already spent" errorTest duplicate token handling

The leading digit carries the meaning: 1x passes, 2x fails, 3x is the awkward case (interactive challenge for a sitekey, already-spent token for a secret key). If you are copying by hand, check the length — every dummy sitekey is 24 characters (1x, 20 zeros, two letters) and every dummy secret key is 35 characters (1x, 31 zeros, AA).

Both halves must match

This is the failure that costs people an afternoon. A test sitekey does not run a challenge; it hands your page the fixed string XXXX.DUMMY.TOKEN.XXXX. That string is meaningless to a production secret key, and a real token is meaningless to a dummy secret key. Cloudflare states it plainly: production secret keys will reject the dummy token, so you must also use a dummy secret key for testing.

If your staging front end uses a test sitekey while your staging back end still holds the live secret key, every submission fails with invalid-input-response — and nothing about the message tells you the two halves disagree.

Cloudflare documents three pairings and what each produces:

Test sitekeyTest secret keyResult
1x00000000000000000000AA1x0000000000000000000000000000000AAAlways succeeds
2x00000000000000000000AB2x0000000000000000000000000000000AAAlways fails
1x00000000000000000000AA3x0000000000000000000000000000000AAAlways fails with timeout-or-duplicate

That third row is the useful one. timeout-or-duplicate is what you get in production when a token is replayed or arrives more than 300 seconds after it was issued, and it is otherwise hard to reproduce deliberately. The 3x secret key gives you a repeatable version of it, so you can test the branch that asks the user to retry and calls turnstile.reset.

Wiring the keys in

Client side, the sitekey is the only thing that changes. Swap the value in data-sitekey:

html
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<form method="POST" action="/subscribe">
  <input type="email" name="email" required>
  <!-- development: always passes -->
  <div class="cf-turnstile" data-sitekey="1x00000000000000000000AA"></div>
  <button type="submit">Subscribe</button>
</form>

Server side, drive both halves from the same environment so they cannot drift apart:

js
const KEYS = {
  development: {
    sitekey: "1x00000000000000000000AA",
    secret: "1x0000000000000000000000000000000AA",
  },
  test: {
    sitekey: "2x00000000000000000000AB",
    secret: "2x0000000000000000000000000000000AA",
  },
  production: {
    sitekey: process.env.TURNSTILE_SITEKEY,
    secret: process.env.TURNSTILE_SECRET_KEY,
  },
};

const { sitekey, secret } = KEYS[process.env.NODE_ENV] ?? KEYS.development;

export async function verify(token, remoteip) {
  const res = await fetch(
    "https://challenges.cloudflare.com/turnstile/v0/siteverify",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ secret, response: token, remoteip }),
    },
  );
  return res.json(); // { success, challenge_ts, hostname, "error-codes": [...] }
}

export { sitekey };

You can exercise the whole server path without a browser at all. The dummy token is a literal, so curl is enough:

bash
# always-pass pair -> {"success":true,...}
curl -s https://challenges.cloudflare.com/turnstile/v0/siteverify \
  -d secret=1x0000000000000000000000000000000AA \
  -d response=XXXX.DUMMY.TOKEN.XXXX

# already-spent pair -> {"success":false,"error-codes":["timeout-or-duplicate"]}
curl -s https://challenges.cloudflare.com/turnstile/v0/siteverify \
  -d secret=3x0000000000000000000000000000000AA \
  -d response=XXXX.DUMMY.TOKEN.XXXX

The siteverify endpoint accepts application/x-www-form-urlencoded and application/json, and always answers with JSON. secret and response are required; remoteip and idempotency_key are optional.

Error codes you will hit while testing

Turnstile returns failures as an error-codes array. Four of them account for nearly every red build:

Error codeMeaningUsual cause in a test run
invalid-input-responseToken is invalid, malformed, or expiredTest sitekey paired with a live secret key, or the reverse
invalid-input-secretSecret key is invalid or expiredTypo in the dummy secret key — count the zeros
missing-input-responseResponse parameter was not providedForm posted before the widget produced a token
timeout-or-duplicateToken has already been validatedToken replayed, older than 300 seconds, or you used the 3x secret key

Worth remembering when you write the assertions: a token is valid for 300 seconds and can be validated exactly once, with a maximum length of 2048 characters. A test suite that validates the same captured token twice will fail the second time for the same reason production would.

What test keys do not cover

Dummy keys remove Turnstile from the picture. That is the point — automated browsers driven by Selenium, Cypress, or Playwright are detected as bots, so a real widget makes tests flaky. But it also means a green suite proves nothing about your real sitekey, your hostname configuration, your widget mode, or how a real challenge behaves under your Content Security Policy.

Three things stay untested until a real token is involved: whether your production sitekey is bound to the hostname you actually serve from, whether Managed mode escalates to a checkbox for your traffic, and whether your uptime checks can reach a page sitting behind a Turnstile WAF challenge. For those you need a genuine solve, not a dummy one.

That is where SolveGate fits. It returns a real Turnstile token for a property you own or are authorised to test — Managed, non-interactive, and invisible widgets, plus Turnstile WAF challenge pages. It does not solve reCAPTCHA, hCaptcha, GeeTest, FunCaptcha, or AWS WAF.

bash
curl -s https://api.solvegate.io/v1/solve \
  -H "Authorization: Bearer $SOLVEGATE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "gate": "turnstile",
    "sitekey": "0x4AAAAAAA...",
    "url": "https://staging.example.com/login"
  }'

A token typically comes back in under 1.5 seconds; GET /v1/solve/{id} polls a job and is free. Credits are prepaid, from $0.40 per 1,000 solves down to $0.075 at volume, failed solves are never billed, and the first 1,000 are free. SDKs are published as solvegate on npm (Node 18+) and PyPI (Python 3.9+). Keep it to properties you own or have written authorisation to test — QA, CI, uptime monitoring, staging, and anti-bot configuration testing.

For everything else, use the dummy keys. They are faster, free, and deterministic, which is exactly what a test suite wants.

Common questions

Yes. Dummy sitekeys work on any domain, including localhost, 127.0.0.1, 0.0.0.0, and any development hostname. Real sitekeys are restricted to the hostnames you configure, and Cloudflare recommends keeping local domains off production sitekeys.

All five dummy sitekeys produce the same fixed string, XXXX.DUMMY.TOKEN.XXXX. Because it is a literal, you can post it straight to siteverify with curl and skip the browser entirely.

Almost always because the two halves do not match. A dummy token is only accepted by a dummy secret key, and a real token is only accepted by a real one. Check that the sitekey in your page and the secret key on your server come from the same environment.

3x00000000000000000000FF, a visible widget. Use it to exercise the path where a visitor has to click, rather than the silent pass that 1x00000000000000000000AA gives you.

Use the secret key 3x0000000000000000000000000000000AA. It always returns {"success": false, "error-codes": ["timeout-or-duplicate"]}, which is the same failure you would see in production from a replayed token or one older than 300 seconds.

No. The always-pass keys accept anyone, so shipping them removes the protection completely. Keep the dummy values in your development and test configuration only, and load production keys from environment variables or a secret manager.

Related

More in Glossary

Automating a gate you own or are authorised to test?

// SolveGate clears Cloudflare Turnstile and WAF challenges through one REST call · first 1,000 solves free