
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'23import React from 'react'4import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'56export const ReCaptchaProvider: React.FC<{7 children: React.ReactNode8}> = ({ children }) => {9 const recaptchaSiteKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY1011 if (!recaptchaSiteKey) {12 console.warn('ReCAPTCHA site key is not configured')13 return <>{children}</>14 }1516 return (17 <GoogleReCaptchaProvider18 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:
- It imports and wraps the app in the
GoogleReCaptchaProviderfrom thereact-google-recaptcha-v3package. - It reads the site key from your environment variables.
- 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'45export const Providers: React.FC<{6 children: React.ReactNode7}> = ({ 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 reCAPTCHA2if (!executeRecaptcha) {3 setError({4 message: 'reCAPTCHA not available. Please try again.',5 })6 return7}89let recaptchaToken: string10try {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 return18}
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: boolean3 challenge_ts?: string4 hostname?: string5 score?: number6 action?: string7 'error-codes'?: string[]8}910export async function verifyRecaptcha(token: string): Promise<ReCaptchaResponse> {11 const secretKey = process.env.RECAPTCHA_SECRET_KEY1213 if (!secretKey) {14 throw new Error('reCAPTCHA secret key is not configured')15 }1617 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 })2425 if (!response.ok) {26 throw new Error('Failed to verify reCAPTCHA')27 }2829 return response.json()30}3132export function getMinScore(): number {33 return parseFloat(process.env.RECAPTCHA_MIN_SCORE || '0.5')34}
This file does two main things:
verifyRecaptcha(token):- Sends the token and your secret key to Google’s
https://www.google.com/recaptcha/api/siteverifyendpoint. - Parses and returns the JSON response, which includes fields such as
success,score,action, and possibleerror-codes.
- Sends the token and your secret key to Google’s
getMinScore():- Reads the
RECAPTCHA_MIN_SCOREfrom the environment (or defaults to 0.5). - This allows you to easily adjust your minimum trust threshold.
- Reads the
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 data6 // eslint-disable-next-line @typescript-eslint/no-explicit-any7 const recaptchaToken = (req.data as any)?.recaptchaToken89 if (!recaptchaToken) {10 throw new Error('reCAPTCHA token is required')11 }1213 try {14 // Verify the reCAPTCHA token15 const recaptchaResult = await verifyRecaptcha(recaptchaToken)1617 // Check if reCAPTCHA verification was successful18 if (!recaptchaResult.success) {19 console.error('reCAPTCHA verification failed:', recaptchaResult['error-codes'])20 throw new Error('reCAPTCHA verification failed')21 }2223 // 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 }3738 return data39 },40 ]41 }42}
This hook does the following:
- Extracts the
recaptchaTokenfrom the incoming request body.
If it’s missing, the submission is immediately rejected. - Calls the
verifyRecaptchafunction 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. - Checks the returned
scoreagainst the minimum allowed value (fromgetMinScore).
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:
- Submit the form normally and confirm the request completes without errors.
- Temporarily invalidate your site key or token and confirm that the server rejects the submission.
- Adjust the
RECAPTCHA_MIN_SCOREin your.envfile 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

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

Discover top Azure DevOps Boards extensions for agile teams in 2025 to boost productivity, automate workflows, and enhance project management.

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