Setting up Turnstile (Cloudflare) for website form protection
Cloudflare Turnstile is a CAPTCHA replacement that displays no tasks to the user. It analyzes browser signals and background behavior, issuing a confirmation token without visual tests. Free, does not require a Cloudflare subscription.
Advantages over reCAPTCHA/hCaptcha
- No visual tasks at all — in most cases, a checkmark simply appears in fractions of a second
- Does not track users for advertising purposes
- Configurable modes: Managed, Non-Interactive, Invisible
- Works without a Cloudflare account for end users
Registration
Keys in Cloudflare Dashboard → Turnstile. Requires site_key and secret_key. Special test keys are available (always pass / always block).
Basic Integration
<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">Submit</button>
</form>
Turnstile automatically adds a hidden cf-turnstile-response field upon successful verification.
Invisible Mode
<div id="turnstile-widget"
class="cf-turnstile"
data-sitekey="YOUR_SITE_KEY"
data-size="invisible"
data-callback="onTurnstileSuccess">
</div>
<script>
function onTurnstileSuccess(token) {
document.getElementById('hidden-token').value = token;
document.getElementById('myForm').submit();
}
// Manual invocation
turnstile.execute('#turnstile-widget');
</script>
Server-Side Verification (Node.js/Express)
const verifyTurnstile = async (token, remoteip) => {
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: process.env.TURNSTILE_SECRET,
response: token,
remoteip,
}),
});
const data = await response.json();
return data.success === true;
};
app.post('/contact', async (req, res) => {
const token = req.body['cf-turnstile-response'];
const valid = await verifyTurnstile(token, req.ip);
if (!valid) {
return res.status(422).json({ error: 'Turnstile verification failed' });
}
// Form processing
});
Server-Side Verification (PHP/Laravel)
$token = $request->input('cf-turnstile-response');
$result = Http::post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => config('services.turnstile.secret'),
'response' => $token,
'remoteip' => $request->ip(),
]);
if (!$result->json('success')) {
return back()->withErrors(['captcha' => 'Verification failed']);
}
React Integration
import { Turnstile } from '@marsidev/react-turnstile';
function ContactForm() {
const [token, setToken] = useState(null);
return (
<form onSubmit={handleSubmit}>
<Turnstile
siteKey={process.env.REACT_APP_TURNSTILE_SITE_KEY}
onSuccess={setToken}
onExpire={() => setToken(null)}
options={{ size: 'invisible' }}
/>
<button type="submit" disabled={!token}>
Submit
</button>
</form>
);
}
Test Keys
| Site Key | Behavior |
|---|---|
1x00000000000000000000AA |
Always passes |
2x00000000000000000000AB |
Always blocks |
3x00000000000000000000FF |
Always requests verification |
Implementation Timeline
3–6 hours for full integration with server-side verification.







