> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heretic.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Challenge widget

> Guard a signup or a bonus claim with a drop-in challenge, verified server-side.

The challenge widget silently measures a visitor, lets a clean one through with no friction, and steps a suspicious one into a physical device ceremony before allowing the action. Your backend confirms the result with a single server-side call. If you have wired Cloudflare Turnstile before, this is the same shape.

## Add the widget

Load the script and drop a container into the form you want to guard.

```html theme={"dark"}
<script src="https://heretic.tech/guard.js" async defer></script>

<form action="/signup" method="POST">
  <!-- your fields -->
  <div class="heretic-guard" data-sitekey="hrtc_live_xxxxxxxxxxxxxxxxxxxxxxxx"></div>
  <button type="submit">Create account</button>
</form>
```

The widget runs the probe, and on success writes a token into a hidden `heretic-response` field inside the form. When the visitor is contradicted, it presents the ceremony first — on a desktop it shows a rolling QR to continue on a phone; on a phone it runs the ritual inline — and writes the token once they pass.

## Prove your domain

The site key is public, so the widget only serves pages on the domain the site is registered to, and only once you have proven you control that domain. In the dashboard, open the site, set its domain, and start verification. You get a token to publish in one of two places:

| Method | Where                                                         | Value                               |
| ------ | ------------------------------------------------------------- | ----------------------------------- |
| DNS    | A `TXT` record at `_heretic.<your-domain>`                    | `heretic-site-verification=<token>` |
| File   | `https://<your-domain>/.well-known/heretic-site-verification` | the token, as the whole body        |

Then press **Check now**. Either proof is enough. A verified site accepts its domain, the `www.` form, and any subdomain. Changing the domain restarts verification. Until a site is verified the widget reports `site-unverified` through `data-error-callback` and issues no token.

## Configure

Attributes on the container, or options to `heretic.render()`.

| Attribute                  | Meaning                                                                                                                                                                                                  |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-sitekey`             | Your public site key (`hrtc_live_…`). Required.                                                                                                                                                          |
| `data-mode`                | `auto` (probe, escalate only the suspicious — default) or `challenge` (always run the ceremony).                                                                                                         |
| `data-escalate`            | Which verdicts pass without a ceremony: `contradicted` (step up contradicted and refused — default), `refused` (step up only refused), `always`, `never`. You set the policy; Heretic states no opinion. |
| `data-action`              | A label for this widget instance, returned by siteverify.                                                                                                                                                |
| `data-cdata`               | Opaque data you attach (e.g. an account reference), returned by siteverify.                                                                                                                              |
| `data-callback`            | Name of a global function called with the token.                                                                                                                                                         |
| `data-error-callback`      | Name of a global function called with an error code.                                                                                                                                                     |
| `data-response-field-name` | Hidden input name (default `heretic-response`).                                                                                                                                                          |

## Explicit rendering

```html theme={"dark"}
<script src="https://heretic.tech/guard.js"></script>
<div id="guard"></div>
<script>
  const id = heretic.render('#guard', {
    sitekey: 'hrtc_live_xxxxxxxxxxxxxxxxxxxxxxxx',
    callback: (token) => { /* enable submit, stash token */ },
  });
</script>
```

`heretic.render(container, options)` returns a widget id. Also available: `heretic.getResponse(id)`, `heretic.reset(id)`, `heretic.remove(id)`, `heretic.execute(id)`, `heretic.isExpired(id)`.

## Test keys

Build against the widget without a real site, a real device, or quota. These keys are reserved and never belong to a tenant.

| Key                                        | Behaviour                                                                                                                                                       |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hrtc_live_000000000000000000000000`       | Site key that always passes: a token arrives at once, with no probe and no ceremony, whatever `data-mode` or `data-escalate` say.                               |
| `hrtc_live_000000000000000000000001`       | Site key that always fails: `data-error-callback` receives `test-fail`.                                                                                         |
| `hrtc_sk_00000000000000000000000000000000` | Secret for siteverify. Accepts only tokens minted by the passing test key, returns `"test": true`, and does not consume them, so the same token verifies again. |

A real secret never accepts a test token, and the test secret never accepts a real one. To exercise the ceremony itself, use a real site key with `data-mode="challenge"`.

## Verify on your server

Never trust the token on the client. When the form arrives, exchange it:

```bash theme={"dark"}
curl https://heretic.tech/v1/siteverify \
  -H "content-type: application/json" \
  -d '{"secret":"hrtc_sk_your_secret_key","response":"<heretic-response>"}'
```

Request parameters (JSON or `application/x-www-form-urlencoded`):

| Field             | Meaning                                                      |
| ----------------- | ------------------------------------------------------------ |
| `secret`          | Your secret key (`hrtc_sk_…`). Never send it to the browser. |
| `response`        | The token from the `heretic-response` field.                 |
| `remoteip`        | The visitor's IP (optional).                                 |
| `idempotency_key` | Re-verify the same token safely under this key (optional).   |

Success response:

```json theme={"dark"}
{
  "success": true,
  "challenge_ts": "2026-09-02T21:15:49.000Z",
  "hostname": "casino.example",
  "action": "signup",
  "cdata": "account-7f3a",
  "verdict": "uncontradicted",
  "challenged": false,
  "outcome": null
}
```

`verdict` is the probe's finding; `challenged` says whether a ceremony ran; `outcome` is the ceremony result (`passed`) when it did. **Check `hostname` against your own domain**, exactly as you would with any siteverify.

Failure response:

```json theme={"dark"}
{ "success": false, "error-codes": ["timeout-or-duplicate"] }
```

| Error code               | Cause                                                                                                  |
| ------------------------ | ------------------------------------------------------------------------------------------------------ |
| `missing-input-secret`   | Secret key not provided.                                                                               |
| `invalid-input-secret`   | Secret key wrong or unknown.                                                                           |
| `missing-input-response` | Token not provided.                                                                                    |
| `invalid-input-response` | Token invalid, malformed, or for another site.                                                         |
| `timeout-or-duplicate`   | Token expired (over 5 minutes) or already used.                                                        |
| `bad-request`            | Malformed request body.                                                                                |
| `internal-error`         | Transient server error; retry with backoff.                                                            |
| `rate-limited`           | Your server's address exceeded 600 verifications a minute. The response is a `429` with `retry-after`. |

## Notes

* Tokens are single-use and valid for 5 minutes, like any challenge token.
* The widget surface is budgeted: 60 calls a minute per visitor address, 1,200 decisions a minute per site across all addresses, and at most 500 ceremonies open per account at once. Over budget, the widget reports `429` through `data-error-callback`; a visitor who waits and retries is served. These are ceilings against floods, not quotas you will meet in ordinary use.
* The site key is public and the widget endpoints are pinned to your site's registered domain, so register the domain your form is served from.
* The widget counts one probe per visitor and one challenge per ceremony against your plan. Read this month's counts, limits, and overage at `GET /v1/usage` with your secret key. Sandbox stops at its limit: the sensor opens no more sessions for your sites and the widget reports `429` with `quota-exceeded` instead of minting a ceremony. Paid plans keep serving; the excess is overage at $0.0025 per probe and $0.025 per challenge.
