Turnstile foundations
Understand the Turnstile flow
Trace widget rendering, browser challenge, token submission, server validation, and the protected application action.
8 minute lesson
Turnstile is Cloudflare’s CAPTCHA alternative. It tells humans and bots apart without making people squint at blurry traffic lights. Most of the time the user does nothing at all. It can run on a site even when the site’s traffic does not use Cloudflare’s CDN.
The protection has two halves, and you need both.
The browser half
The widget renders on your page and obtains a short-lived token:
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form method="POST" action="/submit">
<input type="email" name="email" required />
<div class="cf-turnstile" data-sitekey="your-site-key"></div>
<button type="submit">Sign up</button>
</form>
When the challenge passes, Turnstile adds a hidden field named cf-turnstile-response to the form. That field holds the token.
The server half
The form sends that token to your server. Your server calls Siteverify with your secret key, checks the result, then decides whether to perform the protected action:
const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
body: new URLSearchParams({
secret: env.TURNSTILE_SECRET_KEY,
response: token,
}),
})
const outcome = await result.json()
if (!outcome.success) {
return new Response('Failed the bot check', { status: 403 })
}
Only after outcome.success comes back true do you run the real work: store the signup, send the email, whatever the form protects.
Why the widget alone proves nothing
The widget alone is not a security control because an attacker can call your endpoint directly and skip the page entirely. One curl command posts the form fields without ever rendering your HTML, so a missing server check means no check at all.
There is a mirror-image mistake: calling Siteverify from browser JavaScript. That requires shipping the secret key to the client, where anyone can read it. Siteverify is a server-to-server call. If you ever spot siteverify in client code, treat the secret as leaked and rotate it.
Draw the complete contact-form flow and mark the only component trusted to approve the final submission. It is your server. Everything before that point is just input.
Lesson completed