Three live widgets. One Worker backend. Full end-to-end challenge-response flow you can poke, break, and inspect. Built by Nanosek for Cloudflare partner teams.
Complete all 7 scenarios for full Turnstile mastery. Each card turns green when triggered.
Turnstile is Cloudflare's free replacement for CAPTCHA. Instead of asking users to pick traffic lights or type distorted letters, it runs silent browser checks in the background and decides if the visitor is human. Most of the time, the user sees nothing.
Users click traffic lights. Slow, annoying, blocks accessibility tools, ships data to third parties, and bots already beat it with $0.001/solve farms.
Invisible browser checks. No user clicks in most cases. Privacy-first (no cookies needed). Free & unlimited. Works on any site — Cloudflare proxy not required.
Cloudflare sees ~20% of web traffic. They already know what "normal browsers" and "known bots" look like. Turnstile reuses that signal.
You add one script tag and one div to your page. When your user submits the form, your backend makes one extra HTTP call to Cloudflare to confirm the token. That's it.
The fastest path from zero to a protected form. Follow the steps in order.
Go to dash.cloudflare.com → Turnstile → Add Site. Enter your domain (e.g. yourapp.com). Pick a widget mode — start with Managed. You'll get two keys:
0x4AAAAAAA...0x4AAAAAAA...Paste the Turnstile script and a div with your sitekey inside your form:
When the user submits, the browser sends cf-turnstile-response as a form field.
Never trust the token the browser sends you. Always call Cloudflare's /siteverify endpoint to confirm it's real:
Cloudflare gives you test sitekeys that always pass or always fail — useful for CI:
1x00000000000000000000AA — always passes2x00000000000000000000AB — always blocks3x00000000000000000000FF — forces interactive challenge/siteverify is proof.action per flow (login, signup, checkout) and assert it matches on the backend.idempotency_key so network retries don't get rejected as replays.expired-callback to reset the widget on long forms — tokens live ~300s.timeout-or-duplicate spikes usually mean a frontend bug, not an attack.Turnstile replaces CAPTCHA with invisible browser challenges. Below is the complete data flow — from page load to verified request.
Page loads Turnstile script. Widget sends the public sitekey to the Cloudflare iframe.
Invisible browser checks run inside the iframe. On pass, a single-use token (~300s TTL) is returned.
Token arrives via callback. User submits — token travels to your backend with the form.
Server POSTs token + secret key to Cloudflare's /siteverify endpoint.
Cloudflare returns success: true/false plus metadata: action, challenge timestamp, and error codes.
Cloudflare validates the token against four criteria before returning a verdict:
Never trust the client. Tokens are just strings — anyone can forge one. The only proof is a successful /siteverify call from your server with your secret key.
Cloudflare decides if interaction is needed. May show a checkbox. Best default for most sites.
Shows a "checking…" spinner, no user click. Visible verification without friction.
Zero UI. Runs entirely in background. Best for checkout / API flows where every click costs conversion.
The three situations every engineer shipping Turnstile must know. Each produces a subtle failure in production — here's what happens, why, and how to fix it.
Scenario: user clicks submit twice. Frontend doesn't disable the button. The same token hits /siteverify twice.
token hash within a short TTL. Never re-verify the same token against /siteverify.Scenario: user fills a long form — leaves tab open for 6+ minutes. Token expires client-side. Submit fails with no UX recovery.
expired-callback to call turnstile.reset(id). Always reset before submit on long-lived forms. For checkout, render in invisible mode and re-execute right before submit.Scenario: your backend POSTs to /siteverify. Network hiccups — response never arrives. Retry logic re-sends. Cloudflare sees it as a replay.
idempotency_key (UUID) per user attempt and reuse it across all retries. Cloudflare returns the cached verdict for the same key instead of marking it as duplicate.All three issues look identical in logs: timeout-or-duplicate. Instrument your verify handler with attempt number, idempotency_key, and token age so you can tell them apart when SEV-2s hit.
Turnstile ships with two rendering modes. Pick based on how much control you need and whether your app is SPA-style or server-rendered.
Drop a div with the magic class. Turnstile auto-scans the page, renders the widget, and injects a hidden input[name=cf-turnstile-response] next to your form. Zero JavaScript required.
Load the API with ?render=explicit, then call turnstile.render() yourself. Full control — you decide when to render, reset, remove, and which callbacks fire.
Both rendering modes ship the same cf-turnstile-response token. Your backend code is identical either way:
Always assert data.action matches the flow you expect. A valid login token must not be accepted at /checkout — otherwise the action tag is pointless.
Each tab renders a real Cloudflare Turnstile widget against production infrastructure.
Action tags bind a token to a use-case. Backend asserts the action matches — a login token gets rejected at checkout.
POST to /api/verify. The demo backend calls Cloudflare /siteverify with a real secret and returns the full verdict.
Trigger failures to see exact Cloudflare error codes.
Timing waterfall + full request / response bodies for every verify call.
Every error code Cloudflare returns from /siteverify, what triggers it, and what to do. Cross-reference with the official docs.
secret in POST body. Backend forgot to attach it — check env var is loaded.response (token) in POST body. Form submitted before widget fired callback.idempotency_key — safe.Curated Cloudflare Turnstile docs — everything you need for production deployment.
Ten questions. Feedback explains every answer.