Skip to content
← Back to Blog

How to Integrate Google reCAPTCHA v2 in a Laravel Contact Form

Learn how to integrate Google reCAPTCHA v2 into a Laravel contact form with frontend and server-side validation. This step-by-step guide also covers common reCAPTCHA errors, troubleshooting methods, and production deployment tips.

How to Integrate Google reCAPTCHA v2 in a Laravel Contact Form
How to Integrate Google reCAPTCHA v2 with a Laravel Contact Form

Contact forms are one of the most common targets for automated bots. Spam submissions can fill your inbox, trigger unnecessary emails, and abuse public endpoints.

One practical way to protect a Laravel contact form is Google reCAPTCHA v2 Checkbox, which adds the familiar:

☐ I'm not a robot

verification before allowing the request to be processed.

In this guide, we'll integrate Google reCAPTCHA v2 into a Laravel form, verify the token securely on the server, reset the CAPTCHA after submission, and troubleshoot the most common production errors.


1. Create Google reCAPTCHA Keys

First, register your website with Google reCAPTCHA.

Open the official reCAPTCHA administration page and create a new site.

Choose:

Challenge (v2) → "I'm not a robot" Checkbox

Then add your website domain.

For example:

example.com

Do not enter:

https://example.com/contact

Google expects a hostname/domain rather than a complete URL. A registered domain also covers its first-level subdomains.

After registration, Google provides two credentials:

  • Site Key
  • Secret Key

The Site Key is used in frontend HTML.

The Secret Key must remain private and should only be used by your backend.


2. Store the Keys in Laravel .env

Never hard-code the secret key inside your Blade template, JavaScript, controller, or Git repository.

Add the credentials to .env:

BASH
RECAPTCHA_SITE_KEY=your_site_key_hereRECAPTCHA_SECRET_KEY=your_secret_key_here

Do not commit your production .env file to GitHub.


3. Add reCAPTCHA Configuration

Open:

config/services.php

Add:

BASH
'recaptcha' => [    'site_key' => env('RECAPTCHA_SITE_KEY'),    'secret_key' => env('RECAPTCHA_SECRET_KEY'),],

You can now access the credentials through:

BASH
config('services.recaptcha.site_key')

and:

BASH
config('services.recaptcha.secret_key')

After changing environment/configuration values on production, clear Laravel's cached configuration:

BASH
php artisan optimize:clear

4. Load the Google reCAPTCHA JavaScript

Add Google's reCAPTCHA API script to the page containing your contact form:

BASH
<script    src="https://www.google.com/recaptcha/api.js"    async    defer></script>

Google recommends asynchronous loading because it avoids unnecessarily blocking page rendering.

5. Add the reCAPTCHA Checkbox to the Form

Inside your Laravel Blade form, place the widget before the submit button:

BASH
<form method="POST" action="{{ route('contact.submit') }}">    @csrf    <div>        <label for="name">Name</label>        <input            type="text"            id="name"            name="name"            required        >    </div>    <div>        <label for="email">Email</label>        <input            type="email"            id="email"            name="email"            required        >    </div>    <div>        <label for="message">Message</label>        <textarea            id="message"            name="message"            required        ></textarea>    </div>    <div        class="g-recaptcha"        data-sitekey="{{ config('services.recaptcha.site_key') }}">    </div>    @error('g-recaptcha-response')        <p class="error">            {{ $message }}        </p>    @enderror    <button type="submit">        Send Message    </button></form>

When the visitor completes the CAPTCHA, Google automatically provides a field named:

BASH
g-recaptcha-response

That value must be verified by your backend.

6. Validate That a CAPTCHA Token Exists

Laravel should reject submissions where the visitor hasn't completed reCAPTCHA.

For example:

BASH
$request->validate([    'name' => ['required', 'string', 'max:100'],    'email' => ['required', 'email', 'max:150'],    'message' => ['required', 'string', 'max:5000'],    'g-recaptcha-response' => [        'required',        'string',    ],]);

However, this alone is not sufficient.

A bot could manually send any fake value:

BASH
g-recaptcha-response=fake-token

Therefore, the response must also be verified with Google's server.


7. Create a Server-Side reCAPTCHA Validator

A clean Laravel architecture is to keep CAPTCHA verification inside a dedicated service.

Create:

app/Services/RecaptchaValidator.php

Add:

BASH
<?phpnamespace App\Services;use Illuminate\Support\Facades\Http;class RecaptchaValidator{    public static function verify(        string $token,        ?string $remoteIp = null    ): bool {        $secret = config('services.recaptcha.secret_key');        if (empty($secret) || empty($token)) {            return false;        }        try {            $response = Http::asForm()                ->timeout(10)                ->post(                    'https://www.google.com/recaptcha/api/siteverify',                    [                        'secret' => $secret,                        'response' => $token,                        'remoteip' => $remoteIp,                    ]                );            if (!$response->successful()) {                return false;            }            $data = $response->json();            return ($data['success'] ?? false) === true;        } catch (\Throwable $e) {            report($e);            return false;        }    }}

Google's verification endpoint expects the secret key and user response token. remoteip is optional.


8. Verify reCAPTCHA Before Processing the Form

Now use the validator before sending email or saving anything to the database.

BASH
use App\Services\RecaptchaValidator;use Illuminate\Http\Request;public function submit(Request $request){    $validated = $request->validate([        'name' => ['required', 'string', 'max:100'],        'email' => ['required', 'email', 'max:150'],        'message' => ['required', 'string', 'max:5000'],        'g-recaptcha-response' => ['required', 'string'],    ]);    $verified = RecaptchaValidator::verify(        $request->input('g-recaptcha-response'),        $request->ip()    );    if (!$verified) {        return back()            ->withErrors([                'g-recaptcha-response' =>                    'reCAPTCHA verification failed. Please try again.',            ])            ->withInput();    }    // CAPTCHA passed.    // Send email or save the form here.    return back()->with(        'success',        'Your message has been sent successfully.'    );}

The important rule is:

Form validation

reCAPTCHA server verification

Process request

Send email / save data

Never send the email before CAPTCHA verification.


9. Reset reCAPTCHA After AJAX Submission

If your form uses JavaScript/AJAX and remains on the same page after successful submission, reset the widget:

BASH
if (typeof grecaptcha !== 'undefined') {    grecaptcha.reset();}

For example:

BASH
if (data.success) {    form.reset();    if (typeof grecaptcha !== 'undefined') {        grecaptcha.reset();    }}

This gives the visitor a fresh CAPTCHA for another submission.

This is especially important because reCAPTCHA response tokens are short-lived and single-use. Google states that tokens must be verified within two minutes and cannot be verified more than once.


10. Add Callback Functions for Better UX

You can also detect successful, failed, or expired challenges.

Blade:

BASH
<div    class="g-recaptcha"    data-sitekey="{{ config('services.recaptcha.site_key') }}"    data-callback="onRecaptchaSuccess"    data-error-callback="onRecaptchaError"    data-expired-callback="onRecaptchaExpired"></div>

JavaScript:

BASH
window.onRecaptchaSuccess = function () {    console.log('reCAPTCHA verified.');};window.onRecaptchaError = function () {    console.error('reCAPTCHA verification failed.');};window.onRecaptchaExpired = function () {    console.warn('reCAPTCHA expired.');    if (typeof grecaptcha !== 'undefined') {        grecaptcha.reset();    }};

For production, replace console messages with user-friendly inline messages.


Common Google reCAPTCHA v2 Errors and Their Fixes

ERROR 1 — ERROR for site owner: Invalid domain for site key

This usually means your current domain isn't authorized for that site key.

Go to your reCAPTCHA configuration and verify the domain.

For production:

example.com

For local development, add:

localhost

Google specifically requires localhost to be added when you want to use the key for local development.


ERROR 2 — Invalid site key

Check:

RECAPTCHA_SITE_KEY=

Make sure you haven't accidentally placed the secret key there.

Then run:

BASH
php artisan optimize:clear

You can also verify Laravel sees the configuration:

BASH
php artisan tinker

Then:

BASH
config('services.recaptcha.site_key');

It should not return:

null

ERROR 3 — invalid-input-secret

Google's verification API can return:

invalid-input-secret

This means the secret is invalid or malformed.

Check:

RECAPTCHA_SECRET_KEY=

and:

BASH
config('services.recaptcha.secret_key')

Then clear cached configuration:

BASH
php artisan optimize:clear

ERROR 4 — missing-input-secret

Your application isn't sending the secret key to Google.

Check:

BASH
$secret = config('services.recaptcha.secret_key');

Also verify that config/services.php contains:

BASH
'recaptcha' => [    'site_key' => env('RECAPTCHA_SITE_KEY'),    'secret_key' => env('RECAPTCHA_SECRET_KEY'),],

ERROR 5 — missing-input-response

This means the backend received no CAPTCHA response.

Check that your form contains:

BASH
<div class="g-recaptcha"     data-sitekey="YOUR_SITE_KEY"></div>

Then inspect the submitted request for:

BASH
g-recaptcha-response

ERROR 6 — invalid-input-response

The token sent to Google is invalid or malformed.

Common causes include:

BASH
Wrong site/secret key pairExpired tokenIncorrect frontend integrationToken modified before verification

Generate a fresh CAPTCHA response and try again.


ERROR 7 — timeout-or-duplicate

This is one of the most important reCAPTCHA errors.

It means the token has either:

  • Expired

or:

  • Already been verified once

Google response tokens are valid for two minutes and can only be verified once.

Reset the widget:

BASH
grecaptcha.reset();

Then ask the user to complete the challenge again.


ERROR 8 — grecaptcha is not defined

This normally happens when your JavaScript calls:

BASH
grecaptcha.reset();

before Google's script has loaded.

Avoid:

BASH
grecaptcha.reset();

without checking availability.

Use:

BASH
if (typeof grecaptcha !== 'undefined') {    grecaptcha.reset();}

Because the API loads asynchronously, code that depends on grecaptcha must wait until the API is available. Google specifically warns about this race condition.


ERROR 9 — reCAPTCHA Checkbox Doesn't Appear

First verify the Google script exists:

BASH
<script    src="https://www.google.com/recaptcha/api.js"    async    defer></script>

Then verify the widget:

BASH
<div    class="g-recaptcha"    data-sitekey="YOUR_SITE_KEY"></div>

Also check the browser DevTools console for JavaScript or CSP errors.


11. Content Security Policy Can Block reCAPTCHA

Sites with a strict Content-Security-Policy (CSP) can accidentally block Google's scripts, iframe, or API requests.

If the browser console reports something similar to:

  • Refused to load the script because it violates
  • Content Security Policy

your CSP needs to allow the required Google reCAPTCHA resources.

For example, depending on your implementation:

BASH
script-src:https://www.google.com/recaptcha/https://www.gstatic.com/recaptcha/frame-src:https://www.google.com/recaptcha/connect-src:https://www.google.com/recaptcha/

Don't disable CSP completely just to make CAPTCHA work. Update only the necessary directives.


12. Test the Protection Properly

Don't consider the integration finished just because the checkbox appears.

Test at least these scenarios:

TestExpected Result
Submit without CAPTCHA❌ Rejected
Complete CAPTCHA✅ Accepted
Valid form + CAPTCHA✅ Email sent
Invalid form fields❌ Validation shown
Expired CAPTCHA❌ Rejected / reset
Reuse old token❌ Rejected
Refresh page✅ New CAPTCHA available
Mobile browser✅ Widget works
Production domain✅ No domain error

Most importantly, manually sending a POST request without a valid CAPTCHA token must not bypass the protection.


13. Production Security Checklist

Before deploying, verify:

  1. Site key is allowed to appear in frontend code.
  2. Secret key exists only on the server.
  3. Production .env isn't committed to Git.
  4. Correct production domain is registered.
  5. CAPTCHA is verified server-side.
  6. Email/database processing occurs only after verification.
  7. Expired/duplicate tokens are handled.
  8. AJAX forms reset CAPTCHA after submission.
  9. CSP permits required reCAPTCHA resources.
  10. Laravel configuration cache has been refreshed.
  11. Errors shown to visitors don't expose secret keys or internal exceptions.

The critical concept is simple:

Browser verification ≠ security

Browser verification

+

Server-side Google verification

=

Proper reCAPTCHA protection

A frontend checkbox alone cannot protect a Laravel endpoint because an attacker can bypass the browser and call the endpoint directly.

Conclusion

Google reCAPTCHA v2 is relatively straightforward to integrate into Laravel, but a production-ready implementation requires more than displaying the checkbox.

The complete flow should be:

User fills form

Google reCAPTCHA challenge

g-recaptcha-response generated

Laravel receives request

Laravel validates fields

Backend sends token to Google

Google confirms success

Application processes form

Email/database operation

CAPTCHA reset

With server-side verification, proper key management, error handling, and token reset logic, reCAPTCHA v2 provides a solid additional layer of protection against automated contact-form spam.

More Articles

Laravel How to Build an AI-Powered Laravel App with Laravel AI SDK WordPress Build a Custom Gutenberg CTA Block in TailPress Without ACF