NANOSEK TRAINING LAB
ONLINE
nanosek@training:~$
>
TURNSTILE · V1 · CLOUDFLARE

TURNSTILE
PROTOCOL

HANDS-ON TRAINING LAB

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.

0
Widgets
0
Scenarios
0
Quiz Items
0
Live Traffic
// 01 — PROGRESS

Mission Scoreboard

Complete all 7 scenarios for full Turnstile mastery. Each card turns green when triggered.

—
Managed
—
Non-Int
—
Invisible
—
Dark
—
Action
—
Stale
—
Bad Key
// 02 — WHAT IS TURNSTILE

What is Turnstile?

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.

The Problem It Solves

Old World vs New World

Old: reCAPTCHA / hCaptcha

Users click traffic lights. Slow, annoying, blocks accessibility tools, ships data to third parties, and bots already beat it with $0.001/solve farms.

New: Turnstile

Invisible browser checks. No user clicks in most cases. Privacy-first (no cookies needed). Free & unlimited. Works on any site — Cloudflare proxy not required.

Why Cloudflare can do this

Cloudflare sees ~20% of web traffic. They already know what "normal browsers" and "known bots" look like. Turnstile reuses that signal.

TL;DR

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.

// 03 — GETTING STARTED

5-Minute Setup

The fastest path from zero to a protected form. Follow the steps in order.

Create a Site in Cloudflare Dashboard

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:

  • Sitekey — public, goes in HTML, format 0x4AAAAAAA...
  • Secret key — private, store in env var, format 0x4AAAAAAA...

Add the Widget to Your Page

Paste the Turnstile script and a div with your sitekey inside your form:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <form method="POST" action="/login"> <input name="email" type="email" /> <input name="password" type="password" /> <!-- Turnstile widget --> <div class="cf-turnstile" data-sitekey="0x4AAAAAAA_your_sitekey" data-action="login"></div> <button type="submit">Log in</button> </form>

When the user submits, the browser sends cf-turnstile-response as a form field.

Verify on Your Backend

Never trust the token the browser sends you. Always call Cloudflare's /siteverify endpoint to confirm it's real:

// Node.js / Cloudflare Worker const token = formData.get('cf-turnstile-response'); const body = new URLSearchParams(); body.append('secret', env.TURNSTILE_SECRET); // from env! body.append('response', token); body.append('remoteip', request.headers.get('CF-Connecting-IP')); body.append('idempotency_key', crypto.randomUUID()); const r = await fetch( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body } ); const data = await r.json(); if (!data.success) throw new Error('bot detected'); if (data.action !== 'login') throw new Error('wrong flow'); // OK — proceed with real login logic

Test It

Cloudflare gives you test sitekeys that always pass or always fail — useful for CI:

  • 1x00000000000000000000AA — always passes
  • 2x00000000000000000000AB — always blocks
  • 3x00000000000000000000FF — forces interactive challenge

Cloudflare Best Practices

  1. Never put the secret key in client code. Only the sitekey goes in HTML.
  2. Always verify server-side. A token alone proves nothing — only /siteverify is proof.
  3. Set an action per flow (login, signup, checkout) and assert it matches on the backend.
  4. Send idempotency_key so network retries don't get rejected as replays.
  5. Use expired-callback to reset the widget on long forms — tokens live ~300s.
  6. Rotate the secret key if you ever commit it to git or leak it in logs.
  7. Monitor error-codes. timeout-or-duplicate spikes usually mean a frontend bug, not an attack.
// 04 — ARCHITECTURE

Full Data Flow

Turnstile replaces CAPTCHA with invisible browser challenges. Below is the complete data flow — from page load to verified request.

ARCHITECTURE FLOW · v1
SITEKEY TOKEN VERIFY
YOUR WEBSITE https://yourapp.com/login <script src="challenges.cloudflare.com/turnstile/v0/api.js"> turnstile.render({ sitekey: "0x4AAA..." callback: fn(token) CLOUDFLARE IFRAME ✓ Browser fingerprint ✓ Proof-of-work / challenge ✓ Behavioural signals → Emit single-use token TTL ~300s · single-use sitekey → ← token 1 2 USER submits form form + token 3 ORIGIN / WORKER POST /api/verify + secret (env var) CLOUDFLARE /siteverify challenges.cloudflare.com → { success: true, ... } secret + token verdict JSON 4 5

Sitekey → Cloudflare

Page loads Turnstile script. Widget sends the public sitekey to the Cloudflare iframe.

Challenge → Token

Invisible browser checks run inside the iframe. On pass, a single-use token (~300s TTL) is returned.

Form Submission

Token arrives via callback. User submits — token travels to your backend with the form.

Backend → /siteverify

Server POSTs token + secret key to Cloudflare's /siteverify endpoint.

Verdict

Cloudflare returns success: true/false plus metadata: action, challenge timestamp, and error codes.

What /siteverify Actually Checks

Cloudflare validates the token against four criteria before returning a verdict:

1
Integrity — signature not tampered
2
Challenge — contains a valid pass
3
Freshness — not expired (~300s)
4
Uniqueness — not used before
KEY

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.

Widget Modes

Managed

Cloudflare decides if interaction is needed. May show a checkbox. Best default for most sites.

Non-Interactive

Shows a "checking…" spinner, no user click. Visible verification without friction.

Invisible

Zero UI. Runs entirely in background. Best for checkout / API flows where every click costs conversion.

// 05 — EDGE CASES

Race Conditions & Gotchas

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.

1
Double Submit (Replay)

Scenario: user clicks submit twice. Frontend doesn't disable the button. The same token hits /siteverify twice.

t=0.0sPOST /verify · token=abc…
t=0.3s← success: true ✓
t=0.8sPOST /verify · token=abc… (same)
t=1.1s← timeout-or-duplicate ✗
FIXDisable submit button on click. Or deduplicate server-side by token hash within a short TTL. Never re-verify the same token against /siteverify.
2
Stale Token (Expired)

Scenario: user fills a long form — leaves tab open for 6+ minutes. Token expires client-side. Submit fails with no UX recovery.

t=0swidget token issued · TTL 300s
t=60suser typing long form…
t=340sPOST /verify · token=xyz…
t=340s← timeout-or-duplicate ✗
FIXUse the 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.
3
Retry Without Idempotency Key

Scenario: your backend POSTs to /siteverify. Network hiccups — response never arrives. Retry logic re-sends. Cloudflare sees it as a replay.

attempt 1POST /siteverify (no key)
→ upstream timeout
attempt 2POST /siteverify (no key)
← timeout-or-duplicate ✗
FIXGenerate one 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.
PROD

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.

// 06 — INTEGRATION

Two Ways To Render

Turnstile ships with two rendering modes. Pick based on how much control you need and whether your app is SPA-style or server-rendered.

Implicit Rendering
SSR / FORMS

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.

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <form action="/login" method="POST"> <input name="email" /> <div class="cf-turnstile" data-sitekey="0x4AAA..." data-action="login" data-theme="dark"></div> <button type="submit">Log in</button> </form>
data-sitekey
public key from dashboard
data-action
flow label (login, signup…)
data-theme
light · dark · auto
data-size
normal · flexible · compact
data-callback
global fn name to receive token
✓ traditional forms ✓ minimal JS ✗ SPA / dynamic
Explicit Rendering
SPA / REACT

Load the API with ?render=explicit, then call turnstile.render() yourself. Full control — you decide when to render, reset, remove, and which callbacks fire.

<script src="...api.js?render=explicit" async defer></script> const id = turnstile.render('#widget', { sitekey: '0x4AAA...', action: 'checkout', theme: 'dark', callback: (token) => submitWithToken(token), 'error-callback': () => showError(), 'expired-callback': () => turnstile.reset(id), 'timeout-callback': () => turnstile.reset(id), }); // later: force a fresh token right before submit turnstile.execute(id); turnstile.reset(id); turnstile.remove(id); // on unmount
render(el,opts)
returns widget id
execute(id)
request token (invisible mode)
reset(id)
discard + re-run challenge
remove(id)
teardown on unmount
getResponse(id)
current token or null
✓ React / Vue / SPA ✓ invisible mode ✓ modals / async UI

Shared Backend Contract

Both rendering modes ship the same cf-turnstile-response token. Your backend code is identical either way:

// Cloudflare Worker · /api/verify const form = new URLSearchParams(); form.append('secret', env.TURNSTILE_SECRET); form.append('response', token); form.append('remoteip', request.headers.get('CF-Connecting-IP')); form.append('idempotency_key', crypto.randomUUID()); // SAFE RETRIES const r = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: form }); const data = await r.json(); if (!data.success) throw new Error('verify failed'); if (data.action !== 'login') throw new Error('action mismatch'); // CROSS-FLOW GUARD
RULE

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.

// 07 — LIVE LAB

Widget Playground

Each tab renders a real Cloudflare Turnstile widget against production infrastructure.

Managed
Non-Interactive
Invisible
LIGHT
MANAGED
WAITING
TIP

Action tags bind a token to a use-case. Backend asserts the action matches — a login token gets rejected at checkout.

Verify Token

POST to /api/verify. The demo backend calls Cloudflare /siteverify with a real secret and returns the full verdict.

Failure Scenarios

Trigger failures to see exact Cloudflare error codes.

// 08 — INSPECTOR

Network Trace

Timing waterfall + full request / response bodies for every verify call.

Network Inspector
0 requests
// Waterfall appears after first verify
// Verify a token to populate the trace
// 09 — ERROR CODES

Siteverify Error Reference

Every error code Cloudflare returns from /siteverify, what triggers it, and what to do. Cross-reference with the official docs.

Code
Meaning · Cause · Fix
missing-input-secret
No secret in POST body. Backend forgot to attach it — check env var is loaded.
invalid-input-secret
Secret is malformed or doesn't match any widget. Copy-paste error, mixed test/prod keys, or wrong secret for this sitekey.
missing-input-response
No response (token) in POST body. Form submitted before widget fired callback.
invalid-input-response
Token is invalid, malformed, or forged. Classic bot signature — blocked.
bad-request
Request was malformed. Usually wrong content-type or missing params.
timeout-or-duplicate
Token already consumed or past 300s TTL. See Race Conditions §02 — most common prod error.
internal-error
Cloudflare-side hiccup. Retry with same idempotency_key — safe.
// 10 — DOCUMENTATION

Official Documentation

Curated Cloudflare Turnstile docs — everything you need for production deployment.

Getting Started

First widget in 10 min. Install, sitekey, render.

Guide

Widget Types

Managed vs non-interactive vs invisible.

Compare

Configuration

All params: theme, size, language, retry.

Options

Server Validation

Exchange token for verdict server-side.

Validate

Error Codes

timeout-or-duplicate, invalid-input-secret, etc.

Reference

Pre-Clearance

cf_clearance cookie — skip WAF challenges.

Docs
// 11 — FINAL EXAM

Knowledge Check

Ten questions. Feedback explains every answer.

Question 1 / 10Score: 0
Loading…