FJAN Logo

How to use Google reCAPTCHA v3 with Next.js Payload v3 inside FormBuilder

Date Published

Reading Time

5 minutes

Share

A post hero background with nextjs, payload and recaptcha icons
Profile photo of Funs Janssen

Written by Funs Janssen

Software Consultant

I’m Funs Janssen. I build software and write about the decisions around it—architecture, development practices, AI tooling, and the business impact behind technical choices. This blog is a collection of practical notes from real projects: what scales, what breaks, and what’s usually glossed over in blog-friendly examples.

Integrating Google reCAPTCHA v3 into your Next.js + Payload CMS setup helps protect your forms against spam and automated submissions. This guide walks you through the full process: setting up reCAPTCHA keys, adding a provider on the client side, executing the check during form submission, and verifying the result server-side before accepting the submission.

Preparation

First, install the required NPM package:

pnpm add react-google-recaptcha-v3

This package provides React bindings for Google reCAPTCHA v3, which makes it easy to trigger verification from the client side without manually handling the Google script.

Next, configure the following environment variables in your .env file:

  • NEXT_PUBLIC_RECAPTCHA_SITE_KEY: the public key you get from the Google reCAPTCHA admin console. This key is safe to expose to the client.
  • RECAPTCHA_SECRET_KEY: the private key, used only on the server to verify tokens sent by the client. Keep this secret.
  • RECAPTCHA_MIN_SCORE (optional): reCAPTCHA v3 returns a score between 0.0 and 1.0. A higher score means Google believes the request is more likely to come from a human. You can use this variable to define the minimum accepted score (e.g., 0.5).

Client side setup

On the client, we need to wrap our app with the Google reCAPTCHA provider so that any component can use the useGoogleReCaptcha() hook.

Start by creating a new folder called ReCaptcha inside your providers directory.
Inside that folder, create a file named index.ts.

1'use client'
2
3import React from 'react'
4import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'
5
6export const ReCaptchaProvider: React.FC<{
7 children: React.ReactNode
8}> = ({ children }) => {
9 const recaptchaSiteKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY
10
11 if (!recaptchaSiteKey) {
12 console.warn('ReCAPTCHA site key is not configured')
13 return <>{children}</>
14 }
15
16 return (
17 <GoogleReCaptchaProvider
18 reCaptchaKey={recaptchaSiteKey}
19 scriptProps={{
20 async: true,
21 defer: true,
22 appendTo: 'head',
23 }}
24 >
25 {children}
26 </GoogleReCaptchaProvider>
27 )
28}

This provider does a few things:

  1. It imports and wraps the app in the GoogleReCaptchaProvider from the react-google-recaptcha-v3 package.
  2. It reads the site key from your environment variables.
  3. If the site key isn’t set, it logs a warning and renders the app normally, without reCAPTCHA. This helps avoid build errors in non-production environments.

Next, you’ll want to include this new provider at the top level of your app.
If you already have a root Providers component (often used to wrap ThemeProvider, SessionProvider, etc.), you can simply import and nest it there.

1import { HeaderThemeProvider } from './HeaderTheme'
2import { ThemeProvider } from './Theme'
3import { ReCaptchaProvider } from './ReCaptcha'
4
5export const Providers: React.FC<{
6 children: React.ReactNode
7}> = ({ children }) => {
8 return (
9 <ThemeProvider>
10 <HeaderThemeProvider>
11 <ReCaptchaProvider>{children}</ReCaptchaProvider>
12 </HeaderThemeProvider>
13 </ThemeProvider>
14 )
15}

If you don’t have such a component, you can add the <ReCaptchaProvider> directly in your root layout.tsx or layout.ts file around the main children element.


Using reCAPTCHA in the form

Once the provider is set up, you can trigger reCAPTCHA whenever the user submits a form.
Open your form component and import the useGoogleReCaptcha hook.

At the top of your component, get the executeRecaptcha function:

1const { executeRecaptcha } = useGoogleReCaptcha()

Now, inside your form submission handler (e.g. submitForm), you can execute the reCAPTCHA check before sending the form data to your API. The function executeRecaptcha runs the verification and returns a token string that proves the user interaction passed Google’s checks.

If the function is not yet available (for example, before the reCAPTCHA script finishes loading), you can show an error message and ask the user to retry later.

Then, call executeRecaptcha('form_submit') with an action name. The returned token should be added to your form submission payload.

1// Execute reCAPTCHA
2if (!executeRecaptcha) {
3 setError({
4 message: 'reCAPTCHA not available. Please try again.',
5 })
6 return
7}
8
9let recaptchaToken: string
10try {
11 recaptchaToken = await executeRecaptcha('form_submit')
12} catch (err) {
13 console.warn('reCAPTCHA error:', err)
14 setError({
15 message: 'reCAPTCHA verification failed. Please try again.',
16 })
17 return
18}

Once you have the token, include it in the body of your POST request alongside your form data.

1body: JSON.stringify({
2 form: formID,
3 submissionData: dataToSend,
4 recaptchaToken,
5})

Finally, ensure that your executeRecaptcha reference is added to the dependency list of your form submission callback, so React doesn’t complain about missing dependencies during re-renders.

1[router, formID, redirect, confirmationType, executeRecaptcha]

That’s all for the client side. At this point, every time a form is submitted, the client will request a reCAPTCHA token and include it with the submission.


Server side verification

The server must verify that the token it receives from the client is valid.
To do this, we’ll create a helper file that sends the token to Google’s verification endpoint and checks the result.

Create a new file called verifyRecaptcha.ts.

1interface ReCaptchaResponse {
2 success: boolean
3 challenge_ts?: string
4 hostname?: string
5 score?: number
6 action?: string
7 'error-codes'?: string[]
8}
9
10export async function verifyRecaptcha(token: string): Promise<ReCaptchaResponse> {
11 const secretKey = process.env.RECAPTCHA_SECRET_KEY
12
13 if (!secretKey) {
14 throw new Error('reCAPTCHA secret key is not configured')
15 }
16
17 const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
18 method: 'POST',
19 headers: {
20 'Content-Type': 'application/x-www-form-urlencoded',
21 },
22 body: `secret=${secretKey}&response=${token}`,
23 })
24
25 if (!response.ok) {
26 throw new Error('Failed to verify reCAPTCHA')
27 }
28
29 return response.json()
30}
31
32export function getMinScore(): number {
33 return parseFloat(process.env.RECAPTCHA_MIN_SCORE || '0.5')
34}

This file does two main things:

  1. verifyRecaptcha(token):
  2. getMinScore():
    • Reads the RECAPTCHA_MIN_SCORE from the environment (or defaults to 0.5).
    • This allows you to easily adjust your minimum trust threshold.

Integrating with Payload FormBuilder

Now that the verification hook is ready, you can integrate it into Payload’s form submission logic.
Payload allows you to override form submission behavior using formSubmissionOverrides.

Inside your Payload plugin configuration, add a hook to beforeValidate. This hook will intercept form submissions before they’re validated or saved.

1formSubmissionOverrides: {
2 hooks: {
3 beforeValidate: [
4 async ({ data, req }) => {
5 // Extract reCAPTCHA token from the request data
6 // eslint-disable-next-line @typescript-eslint/no-explicit-any
7 const recaptchaToken = (req.data as any)?.recaptchaToken
8
9 if (!recaptchaToken) {
10 throw new Error('reCAPTCHA token is required')
11 }
12
13 try {
14 // Verify the reCAPTCHA token
15 const recaptchaResult = await verifyRecaptcha(recaptchaToken)
16
17 // Check if reCAPTCHA verification was successful
18 if (!recaptchaResult.success) {
19 console.error('reCAPTCHA verification failed:', recaptchaResult['error-codes'])
20 throw new Error('reCAPTCHA verification failed')
21 }
22
23 // Check the score (v3 returns a score from 0.0 to 1.0)
24 const minScore = getMinScore()
25 if (recaptchaResult.score !== undefined && recaptchaResult.score < minScore) {
26 console.warn(
27 `reCAPTCHA score too low: ${recaptchaResult.score} (minimum: ${minScore})`,
28 )
29 throw new Error(
30 'Suspicious activity detected. Please try again or contact support if the problem persists.',
31 )
32 }
33 } catch (error) {
34 console.error('reCAPTCHA verification error:', error)
35 throw error instanceof Error ? error : new Error('reCAPTCHA verification failed')
36 }
37
38 return data
39 },
40 ]
41 }
42}

This hook does the following:

  1. Extracts the recaptchaToken from the incoming request body.
    If it’s missing, the submission is immediately rejected.
  2. Calls the verifyRecaptcha function to check the token with Google’s API.
    If verification fails or Google returns an error, it throws an exception and stops the form submission.
  3. Checks the returned score against the minimum allowed value (from getMinScore).
    If the score is too low, it assumes the request is suspicious and rejects it.
    You can customize this part to log attempts or trigger alerts if needed.

Make sure you import the verification helpers (verifyRecaptcha and getMinScore) from the file you created earlier.


Testing and verification

At this point, your setup should be functional:

  • When a user submits a form, reCAPTCHA v3 silently scores the interaction.
  • The client sends the reCAPTCHA token with the form submission.
  • The server verifies the token using Google’s API.
  • Submissions that fail verification or have a low trust score are rejected before reaching your form handler.

To test your setup:

  1. Submit the form normally and confirm the request completes without errors.
  2. Temporarily invalidate your site key or token and confirm that the server rejects the submission.
  3. Adjust the RECAPTCHA_MIN_SCORE in your .env file to see how stricter or looser thresholds affect form acceptance.

That’s it! You now have a complete, secure reCAPTCHA v3 integration for forms built with Next.js and Payload’s FormBuilder.
For a working reference implementation, check out the example repository here on my GitHub.


https://github.com/funsjanssen/nextjs-payload-recaptcha-demo/tree/main?tab=readme-ov-file

Comments

No comments yet. Be the first to comment.

Leave a comment

Profile photo of Funs Janssen

Written by Funs Janssen

Software Consultant

I’m Funs Janssen. I build software and write about the decisions around it—architecture, development practices, AI tooling, and the business impact behind technical choices. This blog is a collection of practical notes from real projects: what scales, what breaks, and what’s usually glossed over in blog-friendly examples.

Contents

Curving abstract shapes with an orange and blue gradient
Microservices

Hoe een health check voor je web API je kan helpen om betere software te leveren.