2026-08-09 14:53:46 -04:00
# Next.js (Pages Router)
2026-08-13 21:04:57 -04:00
For older Next.js projects using `pages/` rather than `app/` . The widget renders client-side; siteverify lives in the API route.
2026-08-09 14:53:46 -04:00
```tsx title="pages/signup.tsx"
import Script from "next/script";
export default function SignupPage() {
return (
<>
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" />
2026-08-13 21:04:57 -04:00
<form action="/api/signup" method="POST">
2026-08-09 14:53:46 -04:00
<input name="email" type="email" required />
<div
className="cf-turnstile"
data-sitekey="YOUR_SITEKEY"
2026-08-13 21:04:57 -04:00
data-action="signup"
2026-08-09 14:53:46 -04:00
/>
<button type="submit">Sign up</button>
</form>
</>
);
}
` ``
2026-08-13 21:04:57 -04:00
This native form navigates to the API response, so it does not need client-side reset code.
2026-08-09 14:53:46 -04:00
2026-08-13 21:04:57 -04:00
API route (canonical siteverify):
2026-08-09 14:53:46 -04:00
` ``ts title="pages/api/signup.ts"
import type { NextApiRequest, NextApiResponse } from "next";
2026-08-13 21:04:57 -04:00
const expectedHostnames = new Set(
(process.env.TURNSTILE_HOSTNAMES ?? "")
.split(",")
.map((h) => h.trim())
.filter(Boolean),
);
2026-08-09 14:53:46 -04:00
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const token = req.body["cf-turnstile-response"] ?? req.body.token;
2026-08-13 21:04:57 -04:00
if (expectedHostnames.size === 0) {
return res.status(403).json({ error: "Verification failed" });
}
const remoteip =
(req.headers["x-forwarded-for"] as string | undefined)?.split(",")[0] ??
req.socket.remoteAddress;
const verify = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
2026-08-09 14:53:46 -04:00
method: "POST",
2026-08-13 21:04:57 -04:00
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: process.env.TURNSTILE_SECRET!,
response: token,
...(remoteip ? { remoteip } : {}),
}),
2026-08-09 14:53:46 -04:00
});
2026-08-13 21:04:57 -04:00
const result = await verify.json();
if (
verify.ok !== true ||
result.success !== true ||
result.action !== "signup" ||
!expectedHostnames.has(result.hostname)
) {
2026-08-09 14:53:46 -04:00
return res.status(403).json({ error: "Verification failed" });
}
// process signup
return res.json({ ok: true });
}
` ``
2026-08-13 21:04:57 -04:00
` signup` is the stable action for this surface. Preserve an existing custom migration action and compare the returned action to the same value. Siteverify is mandatory for every widget mode, including pre-clearance. Set ` TURNSTILE_HOSTNAMES` to the deployment-specific frontend hostnames; a production value must not include ` localhost` or ` 127.0.0.1`.
2026-08-09 14:53:46 -04:00
## Substitutions
2026-08-13 21:04:57 -04:00
| Placeholder | Replace with |
| ------------------- | -------------------------------------------------------------------- |
| ` YOUR_SITEKEY` | The widget site key from Step 8 |
| ` TURNSTILE_SECRET` | Env-var name. Value is the secret captured in Step 8, kept off disk. |