Why it's the right call, what it looks like, and how to wire it up via API
Bots try leaked email/password combos from data breaches. Login pages are the #1 target. Turnstile stops automated attempts before they hit your auth backend.
Signup forms without protection get flooded with bot accounts used for spam, abuse, or trial farming. Turnstile ensures only real humans register.
Bots systematically guess passwords. Rate limiting helps, but Turnstile adds a layer that stops the attempts before they even reach your rate limiter.
Bots hammer the "forgot password" endpoint to flood inboxes with reset emails or enumerate valid accounts. Turnstile on reset pages blocks this.
Auth pages are high-value, low-traffic surfaces — perfect for Turnstile. The UX impact is minimal (often invisible), and the security benefit is massive.
Put the widget between the last form field and the submit button. Users naturally scan downward — this feels seamless.
Grey out the submit button until Turnstile passes. This prevents form submission errors and gives clear feedback.
Let Cloudflare decide when to show a challenge. Most users pass silently. Only suspicious traffic sees the widget.
Tokens expire after 300s. If the user idles, re-render the widget automatically instead of showing a cryptic error.
Cloudflare decides whether to show a visible challenge. Most real users pass invisibly. Only suspicious sessions see a checkbox.
Always shows a widget, but the user never needs to interact. It runs browser checks automatically and shows a spinner then a checkmark.
Completely hidden. No UI element at all. Runs entirely in the background. If it fails, you need to handle the fallback yourself.
It gives the best balance — invisible for most users, but challenges bots. Invisible mode works too, but you lose the visual trust signal.
Register your domain and choose widget mode.
POST /accounts/{account_id}/challenges/widgets
{
"name": "Auth Pages",
"domains": ["yourapp.com"],
"mode": "managed"
}
Save the sitekey (public) and secret (private) as env vars. Never commit the secret to git.
# .env
TURNSTILE_SITE_KEY=0x4AAAAAAA...
TURNSTILE_SECRET_KEY=0x4AAAAAAB...
Place the widget between the last input field and the submit button on login, signup, and reset forms.
<!-- After password field, before submit -->
<div class="cf-turnstile"
data-sitekey="0x4AAAAAAA..."
data-callback="onTurnstilePass"
data-expired-callback="onExpire"
></div>
Keep the submit button disabled until Turnstile callback fires. Re-disable on token expiry.
function onTurnstilePass(token) {
submitBtn.disabled = false;
hiddenInput.value = token;
}
function onExpire() {
submitBtn.disabled = true;
turnstile.reset();
}
Before processing login/signup, verify the Turnstile token with Cloudflare. Reject if it fails.
POST https://challenges.cloudflare.com
/turnstile/v0/siteverify
{
"secret": env.TURNSTILE_SECRET_KEY,
"response": req.body.turnstileToken,
"remoteip": req.ip // optional
}
Turnstile passed? Continue with auth. Failed? Return 403, log the attempt, don't reveal why.
if (!turnstileResult.success) {
return res.status(403).json({
error: "Verification failed"
});
}
// Continue with auth logic...
authenticateUser(req.body);
| Action | Method | Endpoint |
|---|---|---|
| Create widget | POST | /accounts/{id}/challenges/widgets |
| List widgets | GET | /accounts/{id}/challenges/widgets |
| Get widget | GET | /accounts/{id}/challenges/widgets/{sitekey} |
| Update widget | PUT | /accounts/{id}/challenges/widgets/{sitekey} |
| Delete widget | DELETE | /accounts/{id}/challenges/widgets/{sitekey} |
| Rotate secret | POST | /accounts/{id}/challenges/widgets/{sitekey}/rotate_secret |
| Action | Method | Endpoint |
|---|---|---|
| Verify token | POST | https://challenges.cloudflare.com/turnstile/v0/siteverify |
| Field | Type | Description |
|---|---|---|
name | string | Human-readable widget name |
domains | string[] | Allowed domains (e.g. ["yourapp.com"]) |
mode | string | "managed" | "non-interactive" | "invisible" |
bot_fight_mode | boolean | Enable bot fight mode (optional) |
offlabel | boolean | Remove Cloudflare branding (Enterprise only) |
| Field | Type | Description |
|---|---|---|
secret | string | Your Turnstile secret key (required) |
response | string | Token from cf-turnstile-response (required) |
remoteip | string | User's IP address (optional, improves accuracy) |
idempotency_key | string | Prevent replay attacks (optional, recommended) |