> ## 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.

# Siteverify endpoint

> Exchange a widget token for its verdict, whether a ceremony ran, and the assessment behind it.

```http theme={"dark"}
POST https://heretic.tech/v1/siteverify
Content-Type: application/json
```

```json theme={"dark"}
{ "secret": "hrtc_sk_...", "response": "<heretic-response>" }
```

Call it from your server. The secret travels in the body, as in Turnstile. Form-encoded bodies are accepted with the same names.

## Request

| Field             | Meaning                                                                                                                 |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `secret`          | Your secret key. Required.                                                                                              |
| `response`        | The token from the `heretic-response` field. Required.                                                                  |
| `idempotency_key` | Lets the same token verify again under the same key. Optional.                                                          |
| `remoteip`        | Accepted for Turnstile compatibility. Not read. The address Heretic measured is in the assessment's `record.client_ip`. |

## Status codes

| Status | Body                                                                                                            |
| ------ | --------------------------------------------------------------------------------------------------------------- |
| `200`  | `success: true` with the fields below, or `success: false` with `error-codes`. Verification failures are `200`. |
| `429`  | `{ "success": false, "error-codes": ["rate-limited"] }` with `retry-after` in seconds.                          |

## Success response

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

<ResponseField name="challenge_ts" type="string" required>
  When the token was issued. Tokens expire 300 seconds later.
</ResponseField>

<ResponseField name="hostname" type="string | null" required>
  Host of the page the widget ran on. Matches the site's proven domain, `www.`, or a subdomain. Unverified for test tokens. Compare it with your domain.
</ResponseField>

<ResponseField name="action" type="string | null" required>
  The widget's `data-action`. `null` after a ceremony.
</ResponseField>

<ResponseField name="cdata" type="string | null" required>
  The widget's `data-cdata`.
</ResponseField>

<ResponseField name="request_id" type="string | null" required>
  The assessment behind the token. Read it at [`GET /v1/verdict`](/api/verdict-endpoint) for the address, identifiers, and findings. `null` for a test token.
</ResponseField>

<ResponseField name="verdict" type="string" required>
  `uncontradicted`, `contradicted`, or `refused`.
</ResponseField>

<ResponseField name="challenged" type="boolean" required>
  `true` when a ceremony ran.
</ResponseField>

<ResponseField name="outcome" type="string | null" required>
  `passed` when `challenged` is `true`, else `null`. No token is issued for a ceremony that did not pass.
</ResponseField>

<ResponseField name="test" type="boolean">
  `true` only with the test secret.
</ResponseField>

## What each path returns

| Token from                         | `challenged` | `verdict`                                         | `outcome` | `request_id`                     | `action`      |
| ---------------------------------- | ------------ | ------------------------------------------------- | --------- | -------------------------------- | ------------- |
| Probe passed under `data-escalate` | `false`      | The probe's verdict                               | `null`    | The probe                        | `data-action` |
| Ceremony passed                    | `true`       | `contradicted`, also when the probe was `refused` | `passed`  | The ceremony device's assessment | `null`        |
| Always-pass test key               | `false`      | `uncontradicted`                                  | `null`    | `null`                           | `data-action` |

A probe whose result never reached Heretic counts as `refused`.

## Single use

A token verifies once. A second call answers `timeout-or-duplicate`, whichever secret presents it.

Send `idempotency_key` when your handler can run twice for one submission. The second call succeeds only with the same key as the first, and returns the same response. A first call without a key cannot be repeated.

## Failure response

```json theme={"dark"}
{ "success": false, "error-codes": ["invalid-input-response"] }
```

| Code                     | Status | Cause                                                                                                             |
| ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------- |
| `missing-input-secret`   | `200`  | `secret` absent.                                                                                                  |
| `invalid-input-secret`   | `200`  | `secret` malformed, unknown, or revoked.                                                                          |
| `missing-input-response` | `200`  | `response` absent.                                                                                                |
| `invalid-input-response` | `200`  | Token malformed, signed before a key rotation, issued to another tenant's site, or a test token on a real secret. |
| `timeout-or-duplicate`   | `200`  | Token older than 300 seconds, or already verified without a matching `idempotency_key`.                           |
| `bad-request`            | `200`  | Body is not JSON or form-encoded.                                                                                 |
| `internal-error`         | `200`  | A store did not answer. Retry with the same `idempotency_key`.                                                    |
| `rate-limited`           | `429`  | More than 600 secret-key requests a minute from your server's address.                                            |

A token Heretic cannot verify is no token.

## Test secret

`hrtc_sk_00000000000000000000000000000000` verifies only tokens from the always-pass test site key, answers `"test": true` and `request_id: null`, and does not consume them. See [test keys](/integration/challenge-widget#test-keys).


## Related topics

- [Verdict endpoint](/api/verdict-endpoint.md)
- [Challenge and usage endpoints](/api/challenge-endpoint.md)
- [Challenge widget](/integration/challenge-widget.md)
- [Heretic documentation](/index.md)
- [Quickstart](/quickstart.md)
