Skip to content

Migration Guide ​

Migrating from reCAPTCHA / hCaptcha / Turnstile to Capcat only takes two changes:

  1. Frontend: swap the <script> URL and replace the verification container with <cap-widget>;
  2. 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 v2hCaptchaTurnstileCapcat
Script URLwww.google.com/recaptcha/api.jsjs.hcaptcha.com/1/api.jschallenges.cloudflare.com/turnstile/v0/api.jscapcat.ai/widget/cap.js
Frontend tag<div class="g-recaptcha"><div class="h-captcha"><div class="cf-turnstile"><cap-widget>
Form token fieldg-recaptcha-responseh-captcha-responsecf-turnstile-responsecap-token
siteverify URLwww.google.com/recaptcha/api/siteverifyapi.hcaptcha.com/siteverifychallenges.cloudflare.com/turnstile/v0/siteverifyapi.capcat.ai/<site-key>/siteverify
Success checksuccess: truesuccess: truesuccess: truesuccess: true

Migrating from reCAPTCHA ​

Frontend ​

html
<!-- 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:

js
// 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? ​

Base Capcat verification can replace v3 with a simple pass/fail decision: proof of work runs silently in the browser, and the user only needs to click the checkbox once—no image puzzle. If you do not enable customer-owned reCAPTCHA v3, replace the old Google threshold check with a check on success. If you still need Google's score, enable the optional BYOK reCAPTCHA v3 layer; authenticated Capcat siteverify then returns the token-bound score described in the server verification guide.

Migrating from hCaptcha ​

Frontend ​

html
<!-- 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.

py
# 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 ​

html
<!-- 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):

js
// 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 5 minutes (20 minutes for sites created before November 2026).
  • remoteip and idempotency_key: the same field names as reCAPTCHA / hCaptcha / Turnstile. As they are enabled for your site, Capcat compares remoteip with the client that solved the challenge and answers retries that carry the same idempotency_key; see Binding the token to your visitor.
  • Compatibility by default; risk is opt-in: the default response still only requires checking success. Customer-owned reCAPTCHA v3 can add its standard score; Account Defense Score mode can add action and versioned risk_* fields when its runtime gate is active. See server-side verification for the distinct score directions and missing-field rules.
  • 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) and api.capcat.ai (verification requests).
  • SPA / custom flows: if you're not using a form submission, listen for the widget's solve event 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.

Built on Cap (Apache 2.0)