<!-- Markdown version of the Capcat doc "Migration Guide" (rendered page: https://capcat.ai/guide/migrate.html). Docs index: https://capcat.ai/llms.txt, full single-file version: https://capcat.ai/llms-full.txt -->

# 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](https://console.capcat.ai) 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

```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](https://capcat.ai/guide/protection.md); authenticated Capcat siteverify then returns the token-bound `score` described in the [server verification guide](https://capcat.ai/guide/siteverify.md).

## 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](https://capcat.ai/guide/siteverify.md#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](https://capcat.ai/guide/siteverify.md) 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](https://capcat.ai/guide/widget.md) 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.
