Migration Guide
Migrating from reCAPTCHA / hCaptcha / Turnstile to Capcat only takes two changes:
- Frontend: swap the
<script>URL and replace the verification container with<cap-widget>; - Server: swap the siteverify URL, the secret, and the form field name you read the token from.
The siteverify request accepts both JSON and form encoding (secret=…&response=…), so your existing reCAPTCHA / hCaptcha client code usually just needs the URL and secret changed to work.
Before you start, create a site in the console to get your site key and secret.
Side-by-side overview
| reCAPTCHA v2 | hCaptcha | Turnstile | Capcat | |
|---|---|---|---|---|
| Script URL | www.google.com/recaptcha/api.js | js.hcaptcha.com/1/api.js | challenges.cloudflare.com/turnstile/v0/api.js | capcat.ai/widget/cap.js |
| Frontend tag | <div class="g-recaptcha"> | <div class="h-captcha"> | <div class="cf-turnstile"> | <cap-widget> |
| Form token field | g-recaptcha-response | h-captcha-response | cf-turnstile-response | cap-token |
| siteverify URL | www.google.com/recaptcha/api/siteverify | api.hcaptcha.com/siteverify | challenges.cloudflare.com/turnstile/v0/siteverify | api.capcat.ai/<site-key>/siteverify |
| Success check | success: true | success: true | success: true | success: true |
Migrating from reCAPTCHA
Frontend
<!-- Before -->
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<form action="/submit" method="POST">
<div class="g-recaptcha" data-sitekey="<recaptcha-site-key>"></div>
<button type="submit">Submit</button>
</form>
<!-- After -->
<script src="https://capcat.ai/widget/cap.js"></script>
<form action="/submit" method="POST">
<cap-widget data-cap-api-endpoint="https://api.capcat.ai/<site-key>/"></cap-widget>
<button type="submit">Submit</button>
</form>Placed inside a <form>, <cap-widget> automatically injects a hidden cap-token field that gets submitted along with the form — the same mechanism as g-recaptcha-response; no onload callback or grecaptcha.render() needed.
Server
Swap the siteverify URL for Capcat's, the secret for the one generated in the console, and the field you read from g-recaptcha-response to cap-token:
// Before
const token = req.body["g-recaptcha-response"];
const r = await fetch("https://www.google.com/recaptcha/api/siteverify", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ secret: RECAPTCHA_SECRET, response: token }),
});
// After — form encoding works as-is; only the URL, secret, and field name changed
const token = req.body["cap-token"];
const r = await fetch("https://api.capcat.ai/<site-key>/siteverify", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ secret: CAPCAT_SECRET, response: token }),
});
const { success } = await r.json();Extra parameters like remoteip are ignored — no need to remove them.
Using reCAPTCHA v3?
There's no direct equivalent to v3's "invisible score" — Capcat doesn't score users; a verification simply passes or fails. But Capcat can directly replace v3 in its typical use cases (form abuse prevention, API abuse prevention): proof of work runs silently in the background, and the user only needs to click a checkbox once — no image puzzles. There's no score or action field on the server side anymore; just replace your threshold check with a check on success.
Migrating from hCaptcha
Frontend
<!-- Before -->
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="<hcaptcha-site-key>"></div>
<!-- After -->
<script src="https://capcat.ai/widget/cap.js"></script>
<cap-widget data-cap-api-endpoint="https://api.capcat.ai/<site-key>/"></cap-widget>Server
hCaptcha's siteverify has the same shape as reCAPTCHA's: swap the URL for https://api.capcat.ai/<site-key>/siteverify, the secret for Capcat's, and the token field from h-captcha-response to cap-token. Both form encoding and JSON are accepted; sitekey / remoteip parameters are simply ignored.
# Before
requests.post("https://api.hcaptcha.com/siteverify",
data={"secret": HCAPTCHA_SECRET, "response": request.form["h-captcha-response"]})
# After
requests.post("https://api.capcat.ai/<site-key>/siteverify",
data={"secret": CAPCAT_SECRET, "response": request.form["cap-token"]})Migrating from Turnstile
Frontend
<!-- Before -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="<turnstile-site-key>"></div>
<!-- After -->
<script src="https://capcat.ai/widget/cap.js"></script>
<cap-widget data-cap-api-endpoint="https://api.capcat.ai/<site-key>/"></cap-widget>Server
Turnstile's siteverify supports both JSON and form encoding, and Capcat supports both too — just change the URL, secret, and field name (cf-turnstile-response → cap-token):
// Before
const r = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ secret: TURNSTILE_SECRET, response: token }),
});
// After
const r = await fetch("https://api.capcat.ai/<site-key>/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ secret: CAPCAT_SECRET, response: token }),
});Migration notes
- Token is single-use: once a siteverify check succeeds, the token becomes invalid immediately; replaying it returns
success: false(matching the behavior of all three providers). It's valid for 20 minutes by default. - No score or action: the response has no
score/action/hostnamefield — just checksuccess. - Domain binding: the equivalent of reCAPTCHA / hCaptcha's domain allowlist. Configure "allowed domains" in your site settings in the console; once bound, verification requests from other origins are rejected outright.
- Content Security Policy (CSP): if your site enforces a strict CSP, allow
capcat.ai(script and wasm) andapi.capcat.ai(verification requests). - SPA / custom flows: if you're not using a form submission, listen for the widget's
solveevent to get the token — see the widget guide for details.
You can run old and new verification side by side during migration: replace the frontend page by page, have the server branch on the token field name to pick which provider to verify against, and remove the old dependency once everything is switched over.