Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 Google reCAPTCHA v3 Validation Tutorial with a Custom Rule

๐Ÿ› ๏ธ Laravel Tutorials5 min read

Laravel 13 Google reCAPTCHA v3 Validation Tutorial with a Custom Rule

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
Laravel 13 Google reCAPTCHA v3 Validation Tutorial with a Custom Rule

To add Google reCAPTCHA v3 validation in Laravel 13, load Google's script with your site key, request a token with grecaptcha.execute() when the form is submitted, send it in a hidden field, and verify it on the server with a custom validation rule that calls Google's siteverify endpoint through Laravel's HTTP client. The rule should check success, the expected action and a minimum score.

Unlike the checkbox version, reCAPTCHA v3 never shows a challenge. It returns a score from 0.0 to 1.0 and leaves the decision to you, so the server-side check is the important part.

Prerequisites

  • Laravel 13 with PHP 8.3 or newer.
  • A reCAPTCHA v3 site key and secret key from the Google reCAPTCHA admin console, with your domains (and localhost for development) registered.
  • A form to protect, such as registration or contact.

Step 1: Store the keys in config

Code
RECAPTCHA_SITE_KEY=your-site-key
RECAPTCHA_SECRET_KEY=your-secret-key
RECAPTCHA_MIN_SCORE=0.5

Add them to config/services.php so the app reads them through config(), which keeps working after php artisan config:cache:

JavaScript
'recaptcha' => [
    'site_key' => env('RECAPTCHA_SITE_KEY'),
    'secret_key' => env('RECAPTCHA_SECRET_KEY'),
    'min_score' => (float) env('RECAPTCHA_MIN_SCORE', 0.5),
],

Google suggests 0.5 as a starting threshold. Review real scores in the admin console after launch and adjust.

Step 2: Create a validation rule

Code
php artisan make:rule Recaptcha
JavaScript
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class Recaptcha implements ValidationRule
{
    public function __construct(
        protected string $action,
    ) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value) || $value === '') {
            $fail('Please confirm you are not a robot.');

            return;
        }

        try {
            $response = Http::asForm()
                ->timeout(5)
                ->post('https://www.google.com/recaptcha/api/siteverify', [
                    'secret' => config('services.recaptcha.secret_key'),
                    'response' => $value,
                    'remoteip' => request()->ip(),
                ]);
        } catch (ConnectionException $e) {
            Log::warning('reCAPTCHA verification unavailable', ['error' => $e->getMessage()]);
            $fail('We could not verify the request. Please try again.');

            return;
        }

        $result = $response->json();

        $passed = ($result['success'] ?? false) === true
            && ($result['action'] ?? null) === $this->action
            && ($result['score'] ?? 0) >= config('services.recaptcha.min_score');

        if (! $passed) {
            Log::info('reCAPTCHA rejected', [
                'action' => $result['action'] ?? null,
                'score' => $result['score'] ?? null,
                'errors' => $result['error-codes'] ?? [],
            ]);

            $fail('Our spam check did not pass. Please try again.');
        }
    }
}

The action check matters. A token created for your newsletter form should not be accepted by the registration form. Logging the score and error codes, without the token itself, helps you tune the threshold later.

If you want to be strict about the domain too, compare $result['hostname'] with your app host.

Step 3: Use the rule in a form request

JavaScript
<?php

namespace App\Http\Requests;

use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;

class ContactRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:100'],
            'email' => ['required', 'email', 'max:255'],
            'message' => ['required', 'string', 'max:2000'],
            'recaptcha_token' => ['required', new Recaptcha('contact')],
        ];
    }
}

Step 4: Request a token in the browser

Tokens expire after two minutes, so request one when the user submits, not when the page loads:

JavaScript
<script src="https://www.google.com/recaptcha/api.js?render={{ config('services.recaptcha.site_key') }}"></script>

<form id="contact-form" method="POST" action="{{ route('contact.store') }}">
    @csrf
    <input name="name" value="{{ old('name') }}" required>
    <input type="email" name="email" value="{{ old('email') }}" required>
    <textarea name="message" required>{{ old('message') }}</textarea>
    <input type="hidden" name="recaptcha_token" id="recaptcha_token">
    @error('recaptcha_token') <p class="error">{{ $message }}</p> @enderror
    <button type="submit">Send</button>
</form>

<script>
document.getElementById('contact-form').addEventListener('submit', function (event) {
    event.preventDefault();
    const form = this;

    grecaptcha.ready(function () {
        grecaptcha.execute('{{ config('services.recaptcha.site_key') }}', { action: 'contact' })
            .then(function (token) {
                document.getElementById('recaptcha_token').value = token;
                form.submit();
            });
    });
});
</script>

The action name in JavaScript (contact) must match the one passed to the rule. Google's terms also require you to show the reCAPTCHA badge or an equivalent notice on the page.

What should you do with low scores?

Rejecting outright is simplest, but it can block real users on VPNs or privacy browsers. Alternatives: accept the submission but flag it for review, require email verification, or show a second step. Start strict on high-risk forms such as registration and looser on low-risk ones.

Common mistakes

  • Verifying in JavaScript only. The browser can be bypassed. The server check is the real protection.
  • Reading env() outside config files. After config:cache, env() returns null in your code. Use config('services.recaptcha...').
  • Executing on page load. If the user takes more than two minutes, the token has expired and verification fails with timeout-or-duplicate.
  • Reusing a token. Each token can be verified once. Request a fresh one for every submit.

How do you verify it works?

Fake Google's response with Http::fake() so tests do not call the real API:

JavaScript
<?php

use Illuminate\Support\Facades\Http;

test('a low score is rejected', function () {
    Http::fake([
        'www.google.com/recaptcha/*' => Http::response(['success' => true, 'action' => 'contact', 'score' => 0.1]),
    ]);

    $this->post('/contact', [
        'name' => 'Ali',
        'email' => 'ali@example.com',
        'message' => 'Hello there',
        'recaptcha_token' => 'fake-token',
    ])->assertSessionHasErrors('recaptcha_token');
});

test('a good score passes', function () {
    Http::fake([
        'www.google.com/recaptcha/*' => Http::response(['success' => true, 'action' => 'contact', 'score' => 0.9]),
    ]);

    $this->post('/contact', [
        'name' => 'Ali',
        'email' => 'ali@example.com',
        'message' => 'Hello there',
        'recaptcha_token' => 'fake-token',
    ])->assertSessionHasNoErrors();
});

FAQ

Do I need a package for reCAPTCHA in Laravel?

No. One validation rule and the HTTP client are enough, and you control the logic.

Can I use reCAPTCHA v3 with Livewire or Inertia?

Yes. Get the token in JavaScript before calling the action or posting the form, and validate it with the same rule.

Is rate limiting still needed?

Yes. Combine reCAPTCHA with Laravel's throttle middleware for defence in depth.

References

Article Index & Topic Tags
LaravelSecurityValidationRecaptcha

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.