CaptchaBox / Docs / Next.js
Add a captcha to Next.js
Last updated 6 November 2026 · By Mohammad AlSaraf
You will add a small client component that shows the widget, a server-only function that checks the token, and use both in a form with a Server Action.
1. Get your keys
Sign in to the CaptchaBox client area (a free account is enough) and add your site with its domain. You get a site key for the page and a secret for your server. Keep the secret out of your HTML and out of git: put it in an environment variable.
Put them in .env.local. Only the site key gets the NEXT_PUBLIC_ prefix, so the secret never reaches the browser:
# Test keys that always pass, on any domain. Replace them with your own from my.natbox.io/captchabox
NEXT_PUBLIC_CAPTCHABOX_SITE_KEY=cb_test_pass
CAPTCHABOX_SECRET=cbs_test_pass
2. The widget component
components/CaptchaBox.tsx loads the script once and renders the widget. Change resetKey to get a fresh token, for example after each submit:
'use client';
import { useEffect, useRef } from 'react';
declare global {
interface Window {
captchabox?: {
render: (el: HTMLElement, opts: Record<string, unknown>) => number;
reset: (el?: HTMLElement) => void;
};
}
}
const SRC = 'https://captcha.natbox.io/v1/widget.js';
// Loads the widget once and renders it into this element. It adds a hidden
// "captchabox-response" field to the surrounding <form>.
// Change `resetKey` (e.g. after each submit) to get a fresh token.
export default function CaptchaBox({ siteKey, theme = 'auto', resetKey }: { siteKey: string; theme?: 'auto' | 'light' | 'dark'; resetKey?: unknown }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const render = () => window.captchabox?.render(el, { sitekey: siteKey, theme });
if (window.captchabox) return void render();
let s = document.querySelector<HTMLScriptElement>(`script[src="${SRC}"]`);
if (!s) {
s = document.createElement('script');
s.src = SRC;
s.async = true;
document.head.appendChild(s);
}
s.addEventListener('load', render, { once: true });
}, [siteKey, theme]);
useEffect(() => {
if (resetKey !== undefined && ref.current && window.captchabox) window.captchabox.reset(ref.current);
}, [resetKey]);
return <div ref={ref} />;
}
3. The server check
lib/captchabox.ts:
// Server-side check. Import only from server code (Server Actions, Route Handlers).
// Each token works once, for 5 minutes.
export async function verifyCaptchaBox(token: unknown): Promise<boolean> {
if (typeof token !== 'string' || token === '') return false;
try {
const res = await fetch('https://captcha.natbox.io/v1/siteverify', {
method: 'POST',
body: new URLSearchParams({ secret: process.env.CAPTCHABOX_SECRET ?? '', response: token }),
cache: 'no-store',
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
return data.success === true;
} catch {
return false;
}
}
4. Use them in a form
The Server Action, app/contact/actions.ts:
'use server';
import { verifyCaptchaBox } from '@/lib/captchabox';
export type State = { ok: boolean; message: string } | null;
export async function sendMessage(_prev: State, formData: FormData): Promise<State> {
if (!(await verifyCaptchaBox(formData.get('captchabox-response')))) {
return { ok: false, message: 'The captcha check failed. Please try again.' };
}
// The visitor passed: send the email, save the message, etc.
return { ok: true, message: 'Thanks, your message was sent.' };
}
The form, app/contact/ContactForm.tsx:
'use client';
import { useActionState } from 'react';
import CaptchaBox from '@/components/CaptchaBox';
import { sendMessage } from './actions';
export default function ContactForm() {
const [state, action, pending] = useActionState(sendMessage, null);
return (
<form action={action}>
{state && <p id="result">{state.message}</p>}
<input name="email" type="email" placeholder="Your email" required />
{/* a token works once, so get a new one after every send */}
<CaptchaBox siteKey={process.env.NEXT_PUBLIC_CAPTCHABOX_SITE_KEY!} resetKey={state} />
<button disabled={pending}>Send</button>
</form>
);
}
Render <ContactForm /> on any page. If the visitor presses Send before the check is done, the widget holds the submit and sends it as soon as the token is ready.
With a Route Handler instead
// app/api/contact/route.ts
import { verifyCaptchaBox } from '@/lib/captchabox';
export async function POST(req: Request) {
const { email, token } = await req.json();
if (!(await verifyCaptchaBox(token))) return Response.json({ error: 'captcha' }, { status: 400 });
// the visitor passed
return Response.json({ ok: true });
}
In the browser, get the token with await window.captchabox!.execute() and send it with your JSON.
Trying it out? The test keys cb_test_pass (site key) and cbs_test_pass (secret) always pass, on any domain including localhost. Swap in your own keys from the client area before going live.
Other platforms: all CaptchaBox docs. Questions? Open a ticket, we reply within 24 hours.