“How do I add a CAPTCHA to a Next.js form?”
How do I add a CAPTCHA to a Next.js form?
Short answer
Load widget.js with next/script, render the widget div inside your form, and verify the token on the server before saving. In the App Router that means a small client component for the form, and either a Server Action or a Route Handler that posts the token to https://captchakit.com/api/verify with your secret key and rejects the submission when success is false.
Steps
- 1
Load the widget script
In your page component, load https://captchakit.com/widget.js with next/script using strategy beforeInteractive, as the CaptchaKit docs show for Next.js.
- 2
Add the widget to the form
Inside your form, render a div with data-gamecaptcha, your site key and a game, plus a hidden input that will carry the token.
- 3
Copy the token on submit
On submit, read the token from the widget div's data-token attribute and write it into the hidden input before the action runs.
- 4
Verify in an Action or Route Handler
Post the token and site key to the verify endpoint with your secret key in a Bearer header. If success is false, reject the submission before any side effect.
The page
A server component that loads the script:
// app/signup/page.tsx
import Script from 'next/script';
import SignupForm from './SignupForm';
export default function SignupPage() {
return (
<>
<Script
src="https://captchakit.com/widget.js"
strategy="beforeInteractive"
/>
<main>
<h1>Create an account</h1>
<SignupForm />
</main>
</>
);
}The form component
A client component that copies the token into the hidden field when the form is submitted:
// app/signup/SignupForm.tsx
'use client';
import { useRef } from 'react';
import { signup } from './actions';
export default function SignupForm() {
const widget = useRef<HTMLDivElement>(null);
return (
<form action={signup} onSubmit={(e) => {
const token = widget.current?.dataset.token ?? '';
(e.currentTarget.elements.namedItem('gamecaptchaToken') as HTMLInputElement).value = token;
}}>
<input type="email" name="email" required />
<div ref={widget}
data-gamecaptcha
data-site-key="gc_connect4_your_key_here"
data-game="connect4" />
<input type="hidden" name="gamecaptchaToken" />
<button type="submit">Sign up</button>
</form>
);
}The Server Action
The action receives the FormData, verifies the token, and only then touches your database:
// app/signup/actions.ts
'use server';
export async function signup(formData: FormData) {
const email = String(formData.get('email') ?? '');
const token = String(formData.get('gamecaptchaToken') ?? '');
const verify = await fetch('https://captchakit.com/api/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.CAPTCHAKIT_SECRET_KEY}`,
},
body: JSON.stringify({
token,
siteKey: process.env.CAPTCHAKIT_SITE_KEY,
}),
});
const { success } = await verify.json();
if (!success) throw new Error('Captcha failed');
// token is valid, create the account
await createUser(email);
}The Route Handler option
If you submit with fetch instead of a form action, verify in a Route Handler instead. The pattern is the same: read gamecaptchaToken from the request body, call the verify endpoint, and return a 400 when success is false. The docs include this variant for Next.js.
Either way, keep the secret key in a server-side environment variable such as CAPTCHAKIT_SECRET_KEY. Never prefix it with NEXT_PUBLIC_, which ships the value to the browser.
Questions
Server Action or Route Handler?
Both work. Use an Action for a plain form that works without JavaScript, a Route Handler when you submit with fetch.
Can I verify the token in a client component?
No. The verify call needs your secret key, which must never reach the browser. Verify on the server.
Which next/script strategy should I use?
The CaptchaKit docs use strategy beforeInteractive for the widget script.
Does the widget work in the Pages Router?
The widget is a plain script tag and div, so it renders anywhere. Verify the token in an API route or any server you control.
How do I run it locally?
Put both keys in .env.local, run the dev server, and use a real site key so the widget can issue tokens.
Why did verification fail in my test?
Usually a reused token or one older than 10 minutes. Play again and resubmit. See token expired or already used.
Sources
- CaptchaKit documentation (checked 2026-10-18)
- Node.js fetch API reference (checked 2026-10-18)
Related
Protect your forms with a game
Every arcade CAPTCHA we make. Sign up, grab your embed code, protect your forms today.
Get started →Pay what you want. Start free.