“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.

1Load the widgetscript2Add the widgetto the form3Copy the tokenon submit4Verify in anAction or Route…

Steps

  1. 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. 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. 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. 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

  1. CaptchaKit documentation (checked 2026-10-18)
  2. 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.