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.
| Sitekey | Behaviour | Widget type | Use case |
|---|---|---|---|
| 1x00000000000000000000AA | Always passes | Visible | Test successful form submissions |
| 2x00000000000000000000AB | Always fails | Visible | Test error handling and retry logic |
| 1x00000000000000000000BB | Always passes | Invisible | Test invisible widget success flows |
| 2x00000000000000000000BB | Always fails | Invisible | Test invisible widget error handling |
| 3x00000000000000000000FF | Forces interactive challenge | Visible | Test user interaction scenarios |
The secret keys go on your server, in the call to the siteverify API.
| Secret key | Behaviour | Use case |
|---|---|---|
| 1x0000000000000000000000000000000AA | Always passes validation | Test successful token validation |
| 2x0000000000000000000000000000000AA | Always fails validation | Test validation error handling |
| 3x0000000000000000000000000000000AA | Returns "token already spent" error | Test 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 sitekey | Test secret key | Result |
|---|---|---|
| 1x00000000000000000000AA | 1x0000000000000000000000000000000AA | Always succeeds |
| 2x00000000000000000000AB | 2x0000000000000000000000000000000AA | Always fails |
| 1x00000000000000000000AA | 3x0000000000000000000000000000000AA | Always 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:
<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:
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:
# 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 code | Meaning | Usual cause in a test run |
|---|---|---|
invalid-input-response | Token is invalid, malformed, or expired | Test sitekey paired with a live secret key, or the reverse |
invalid-input-secret | Secret key is invalid or expired | Typo in the dummy secret key — count the zeros |
missing-input-response | Response parameter was not provided | Form posted before the widget produced a token |
timeout-or-duplicate | Token has already been validated | Token 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.
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