Cloudflare Turnstile Laravel - Protect your app from bots

Ivan Radunovic

Feb 27, 20247 min read
Cloudflare Turnstile Laravel - Protect your app from bots

Cloudflare Turnstile is free privacy-friendly replacement for CAPTCHAs. Turnstile stops abuse and assures visitors are real persons.

Compared to Google ReCaptcha, Cloudflare does not share data collected with advertisers.

In some cases bots can beat Google Captcha but they're failing on Turnstile.

Setup for Cloudflare Turnstile

As in every Laravel tutorial I'll use Laravel Breeze starter kit in a Blade-flavor.

The most important form on every site is a sign-up form, that's why I decided to secure register form with Cloudflare Turnstile.

First thing I need to obtain site kyes.

Cloudflare Turnstile obtaining site keys

Go to your Cloudflare Dashboard and from the sidebar choose Turnstile.

Turnstile page in Cloudflare Dashboard

Choose to add new site. You can name your Turnstile integration using some friendly name. In the domain input you can select 1 or more domains.

Regarding Widget Mode the default one Managed is good.

Add Site to Turnstile

After clicking the Create button Cloudflare will display keys.

Site added to Turnstile Keys Generated

Add site keys to Laravel

Inside config/services.php add configuration:

    'turnstile' => [
        'key' => env('TURNSTILE_SITE_KEY'),
        'secret' => env('TURNSTILE_SECRET_KEY')

And add respective keys in the .env file:


Laravel Turnstile Client

I like to keep my custom logic inside app/Logic folder in the dedicated namespace, so I'll create Turnstile/TurnstileClient.php inside.


namespace App\Logic\Turnstile;

use Illuminate\Support\Facades\Http;

class TurnstileClient
    public function check(string $response): CheckResponse
        $response = Http::retry(3, 100)
            ->post('', [
                'secret' => config('services.turnstile.secret'),
                'response' => $response,

        if (! $response->ok()) {
            return new CheckResponse(success: false, errorCodes: []);

        return new CheckResponse(success: $response->json('success'), errorCodes: $response->json('error-codes'));

This class has a check method which passes response and a secret key to the Cloudflare Turnstile API.

Based on the response it'll return CheckResponse object with or without error codes.

CheckResponse class is very simple:


namespace App\Logic\Turnstile;

use Illuminate\Contracts\Support\Arrayable;

class CheckResponse implements Arrayable
    public function __construct(
        public readonly bool $success,
        public readonly array $errorCodes,
    ) {

    public function toArray(): array
        return [
            'success' => $this->success,
            'error-codes' => $this->errorCodes,

With this our Turnstile client is ready and we can implement it inside custom Laravel validation rule.

Turnstile Custom Laravel Validation Rule

In this tutorial I'll implement validation rule in Register logic but it can be implemented into any other validation flow you may have.

Create a rule:

php artisan make:rule Turnstile

And add Turnstile client call inside:


namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use App\Logic\Turnstile\TurnstileClient;

class Turnstile implements Rule
    protected array $messages = [];

    public function __construct(
    ) {

    public function passes($attribute, $value)
        $client = new TurnstileClient();
        $response = $client->check($value);

        if ($response->success) {
            return true;

        foreach ($response->errorCodes as $errorCode) {
            $this->messages[] = $this->mapErrorCodeToMessage($errorCode);

        return false;

    public function message()
        return $this->messages;

    protected function mapErrorCodeToMessage(string $code): string
        return match ($code) {
            'missing-input-secret' => 'The secret parameter was not passed.',
            'invalid-input-secret' => 'The secret parameter was invalid or did not exist.',
            'missing-input-response' => 'The response parameter was not passed.',
            'invalid-input-response' => 'The response parameter is invalid or has expired.',
            'bad-request' => 'The request was rejected because it was malformed.',
            'timeout-or-duplicate' => 'The response parameter has already been validated before.',
            'internal-error' => 'An internal error happened while validating the response.',
            default => 'An unexpected error occurred.',

Integrate Turnstile Rule into Register logic

Go visit Auth\RegisteredUserController and the validate array add:

            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
            'cf-turnstile-response' => ['required', new Turnstile()]

Now on every submit Laravel will call Turnstile validation rule.

The only thing left is to display it on the front end.

Implementing Turnstile into Form

In head section add JS script:

    <script src="" async defer></script>

And in the form under password input add Turnstile section:

       <div class="cf-turnstile" data-sitekey="{{ config('services.turnstile.key') }}"></div>

That's it.

Now Cloudflare Turnstile will be rendered on the Register page:

Laravel Register form with Cloudflare Turnstile protection

More resources for Laravel Sign up pages

If you're looking how to implement social OAuth logins into your Laravel app see Laravel OAuth Authentication.

If you want magic login links see Login into Laravel application with a Magic link.

After implementing this tutorial if you're not happy with Cloudflare Turnstile go read our guide on Google ReCaptcha.

Author Photo
Ivan Radunovic is a Senior Developer with over 11 years of experience in web development. His expertise are Laravel SaaS solutions. So far he developed or took part in 300+ Laravel projects.

More Laravel tutorials

Automatically purge Cloudflare Cache on new Deployment

After new deployment and compiling it's important to clear Cloudflare cache, otherwise it'll serve old assets.

Feb 29, 2024 Ivan Radunovic

Login into Laravel application with a Magic link

Magic link authentication is a popular choice for login pages. User can request a link and click on it and he'll be authenticated.

Feb 24, 2024 Ivan Radunovic

Send Laravel notifications to Telegram messenger

Keeping track of your Laravel application is super important, integrating Laravel with Telegram messenger will help you gain better real-time insights.

Feb 21, 2024 Ivan Radunovic

Improving Laravel Loading Speed Using Redis Cache

Using Redis with Laravel application should be mandatory in the production. Redis greatly improves performance of Laravel apps if it's implemented in the right way.

Feb 20, 2024 Ivan Radunovic

Laravel SSO authentication with Google One Tap logins

Single sign-on authentication became a standard so we teach you how to implement it in your app. On top of that we'll show you how to integrate Google One Tap authentication flow also.

Feb 13, 2024 Ivan Radunovic

Google ReCaptcha in Laravel - Protect Laravel forms from bots

Learn how to integrate Google ReCAPTCHA into Laravel application, applies for types v2 and v3. All about I'm not a robot checkbox, invisible captcha and score based captcha.

Feb 12, 2024 Ivan Radunovic

Advanced querying with whereHas Method in Laravel Eloquent

The whereHas method in Laravel allows to filter models based on conditions of their related models, simplifying complex queries involving relationships.

Feb 05, 2024 Ivan Radunovic

Soft Delete in Laravel

With Laravel soft delete trait you can mark certain Eloquent model as deleted, and they will be removed from general queries. But you can query them with special methods.

Feb 04, 2024 Ivan Radunovic