# Migrate from hCaptcha

> Replace an hCaptcha widget and its siteverify call with AgentGate, attribute by attribute and field by field.

hCaptcha's widget and server check map almost one to one onto AgentGate's.
Visitors stop solving image puzzles: most see nothing, and the rest get a
short press-and-hold check with a no-pointer alternative.

> [!NOTE]
> hCaptcha names below are from hCaptcha's public documentation. Check them
> against the version you use.

## 1. Create a site

Add your site in the [console](/docs/get-started#1-create-a-site) with every
origin that serves the form, and one **action** per form. You get a site key
(`site_…`) and a backend secret (`ags_…`).

## 2. Swap the widget

```html title="Before (hCaptcha)"
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<form method="post" action="/signup">
  <div class="h-captcha" data-sitekey="10000000-ffff-ffff-ffff-000000000001" data-callback="onToken"></div>
  <button>Sign up</button>
</form>
```

```html title="After (AgentGate)"
<script src="https://agentgate.example/sdk/v1/agentgate.js" async></script>
<form method="post" action="/signup">
  <div class="agentgate" data-sitekey="site_…" data-action="signup" data-mode="managed"></div>
  <button>Sign up</button>
</form>
```

| hCaptcha | AgentGate | Notes |
| --- | --- | --- |
| `class="h-captcha"` | `class="agentgate"` | |
| `data-sitekey` | `data-sitekey` | |
| none | `data-action` | required; configured on the site |
| `data-size="invisible"` | `data-mode="invisible"` (default) | |
| normal checkbox widget | `data-mode="managed"` or `"interactive"` | |
| `data-callback`, `data-expired-callback`, `data-error-callback` | `callback`, `expiredCallback`, `errorCallback` options of `AgentGate.render()` | |
| `data-chalexpired-callback`, `data-open-callback`, `data-close-callback` | none | a closed check rejects with `canceled` |
| `h-captcha-response` (and `g-recaptcha-response`) | `agentgate_token` | |
| `hl` | `data-lang` | |
| `data-theme`, `data-size` (compact) | none | system colours |
| `hcaptcha.render`, `execute`, `reset`, `remove`, `getResponse` | `AgentGate.render`, `execute({action})`, `reset`, `remove`, `getResponse` | `execute()` resolves with the token |

In your CSP, replace the hCaptcha hosts (including `frame-src`) with your
AgentGate endpoint in `script-src` and `connect-src`, plus
`worker-src blob:`.

## 3. Swap the server call

```sh title="Before (hCaptcha)"
curl https://api.hcaptcha.com/siteverify \
  -d secret=0x… -d response="$TOKEN" -d sitekey=… -d remoteip="$IP"
```

```sh title="After (AgentGate)"
curl https://agentgate.example/v1/siteverify \
  -H "Authorization: Bearer $AGENTGATE_SECRET" -H "Content-Type: application/json" \
  -d '{"token":"'"$TOKEN"'","expectedAction":"signup","expectedOrigin":"https://app.example.com"}'
```

| hCaptcha | AgentGate | Notes |
| --- | --- | --- |
| `secret` | `Authorization: Bearer ags_…` | or a `secret` JSON field |
| `response` | `token` | |
| `sitekey` | none | the secret identifies the site |
| `remoteip` | none | |
| form-encoded body | JSON body | |
| `success` | `success` | |
| `challenge_ts` | `issuedAt` (Unix milliseconds) | |
| `hostname` | `origin`; enforce with `expectedOrigin` | |
| `credit`, `score`, `score_reason` | none | the rules decide |
| `error-codes` | `error` | see below |

| hCaptcha error | AgentGate error |
| --- | --- |
| `missing-input-secret`, `invalid-input-secret`, `sitekey-secret-mismatch` | `invalid_credential` (401) |
| `missing-input-response`, `bad-request` | `invalid_request` (400) |
| `invalid-input-response` | `invalid_token` (422) |
| `invalid-or-already-seen-response` | `token_used` (409) or `expired_token` (422) |

## 4. Differences to plan for

- **Action required.** Every token is bound to an action; pass the same one
  as `expectedAction`.
- **Token lifetime.** Two minutes by default; the widget verifies at
  submit time.
- **Monitor mode first.** New sites pass every valid token and report
  `wouldDecision`; switch to enforce when ready.
- **Test keys.** hCaptcha's test site key and secret map to AgentGate's
  [testing keys](/docs/testing).
