How to Add Cloudflare Turnstile to Laravel in 5 Minutes (No Packages Required)
If you’ve built a public-facing web app, you eventually run into the same annoying problem: bots. They fill out your forms, spam your inbox, and waste your server’s time.
Most developers immediately think of Google reCAPTCHA, but there’s a cleaner option that solves the same problem without the tracking baggage: Cloudflare Turnstile.
Turnstile is Cloudflare’s privacy-friendly CAPTCHA alternative.
- ❌ No user friction.
- 🚍 No weird “click the traffic lights” puzzles. (ehem.. Google)
- ⏲️ And setup takes only a few minutes.
In this quick guide, I’ll show you why you should use Turnstile — and how to integrate it into a Laravel app without any third-party packages.
Why use Cloudflare Turnstile?
Here are the biggest reasons I migrated to it:
1. No user frustration
Turnstile doesn’t make users solve puzzles. It verifies silently in the background unless something looks suspicious.
2. Privacy-respecting
Unlike reCAPTCHA, Turnstile doesn’t track users across the web. No profiling, no annoying consent banners.
3. Dead simple to implement
It’s literally:
- Load a script
- Place a widget
- Verify the token server-side
Done!
4. Completely free
Cloudflare offers Turnstile with unlimited usage on the free plan.
Step 1 — Create a Turnstile Widget in Cloudflare
- Log in to Cloudflare
- Application Security → Turnstile
- Click Add Widget
- Set:
- Name: e.g: my-domain-name-turnstile
- Add Hostnames: If you already have existing hostnames setup in Cloudflare you can select it, or you can just type your domain name manually. You can also add your local domains here - very useful if you are using Herd on your local machine.
- Widget Mode: Managed (recommended)
- Would you like to opt for pre-clearance for this site?: Just select "No"
- Copy your Site Key and Secret Key
Step 2 — Add the Keys, Widget and Script into Laravel
Add these to your .env file
# Cloudflare Turnstile Captcha
TURNSTILE_ENABLED=true # This will be so that you can just disable it quickly, especially on your tests
TURNSTILE_URL=https://challenges.cloudflare.com/turnstile/v0/siteverify
TURNSTILE_SITE_KEY=Your-Turnstile-Site-Key
TURNSTILE_SECRET_KEY=Your-Turnstile-Secret-Key
Then, configure these values on your config/services.php file
// On services.php
'turnstile' => [
'enabled' => env('TURNSTILE_ENABLED', false),
'turnstile_url' => env('TURNSTILE_URL'),
'site_key' => env('TURNSTILE_SITE_KEY'),
'secret_key' => env('TURNSTILE_SECRET_KEY'),
],
Then add the Widget into your Blade template — this usually goes into public facing forms such as registration, forgot password, contact us, etc.
<div class="cf-turnstile" data-sitekey="{{ config('services.turnstile.site_key') }}">
</div>
Then add their script outside the body of your base template
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
Step 3 — Create a Rule class and use it on your form validations
This sample rule class below provides a way to ignore required validation on the Turnstile field if the config('services.turnstile.enabled') is set to false.
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\Http;
class Turnstile implements Rule
{
/**
* Get the validation rules for the Turnstile field.
* Returns an array with 'required' if Turnstile is enabled, otherwise just the rule.
*
* @return array
*/
public static function rules(): array
{
$rules = [new self()];
if (config('services.turnstile.enabled', false)) {
array_unshift($rules, 'required');
}
return $rules;
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
// If Turnstile is not enabled, skip validation
if (!config('services.turnstile.enabled', false)) {
return true;
}
// If Turnstile is enabled but value is empty, validation fails
if (empty($value)) {
return false;
}
try {
$response = Http::asForm()->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => config('services.turnstile.secret_key'),
'response' => $value,
'remoteip' => request()->ip(),
]);
$result = $response->json();
return isset($result['success']) && $result['success'] === true;
} catch (\Exception $e) {
return false;
}
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The Turnstile verification failed. Please try again.';
}
}
The add the validation field on your controller
// On your Controller...
public function __invoke(Request $request)
{
$request->validate([
'cf-turnstile-response' => Turnstile::rules(),
]);
}
Turnstile sends the token as cf-turnstile-response, so the validation rule catches empty/missing tokens before the request even reaches Cloudflare.
Step 4 — Done. Your form is now protected.
Once this is in place, bots will struggle to submit your forms — and your legitimate users won’t even notice the protection.
You also avoid the privacy overhead and UI friction of Google’s CAPTCHA.
Final Thoughts
After setting up Turnstile on my Laravel apps, the difference was immediate:
- Zero spam submissions
- No broken experiences on mobile
- No "pick all the buses" nightmares
- Everything runs smoothly behind the scenes
If you’re looking for a modern, developer-friendly CAPTCHA alternative, Turnstile is an easy win!