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

# Clearance

> Clear a visitor once and verify the record at your own edge.

A clearance is one signed record of a probe, or of a probe and the ceremony that followed it. It is a JWT signed with Ed25519 under a key pair that belongs to your site. Your server or edge verifies it with the site's public key from [`GET /v1/clearance/keys`](/api/clearance-keys); Heretic is never in the request path. Heretic signs what it measured, including a contradicted verdict, and never withholds or reads back the token.

The token is 900 to 1,200 bytes as a cookie value. Issuing one counts as one probe plus, when a ceremony is minted, one challenge against your [plan](/reference/pricing).

## Hosted page

Redirect an uncleared request to the hosted page. It runs the probe, escalates per the site's clearance settings, and returns to your URL with `heretic_clearance=<jwt>` appended to the query.

```text theme={"dark"}
https://heretic.tech/clearance?sitekey=hrtc_live_…&return=https://shop.example/account
```

`return` must be `https://`, or loopback `http://`, on the site's registered domain. Any other value is refused on the page and never redirected.

Your gate verifies the token, sets its cookie, and redirects to strip the parameter.

```js theme={"dark"}
import { createRemoteJWKSet, jwtVerify } from 'jose';

const SITE_KEY = 'hrtc_live_…';
const DOMAIN = 'shop.example';
const JWKS = createRemoteJWKSet(new URL(`https://heretic.tech/v1/clearance/keys/${SITE_KEY}`));

async function verify(jwt) {
  const { payload } = await jwtVerify(jwt, JWKS, {
    issuer: 'https://heretic.tech', audience: DOMAIN, algorithms: ['EdDSA'],
  });
  return payload;
}

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const returned = url.searchParams.get('heretic_clearance');
    url.searchParams.delete('heretic_clearance');
    if (returned && await verify(returned).catch(() => null)) {
      return new Response(null, { status: 303, headers: {
        location: url.toString(),
        'set-cookie': `heretic_clearance=${returned}; Path=/; Secure; HttpOnly; SameSite=Lax`,
      } });
    }
    const cookie = (request.headers.get('cookie') || '').match(/(?:^|;\s*)heretic_clearance=([^;]+)/)?.[1];
    const payload = cookie ? await verify(cookie).catch(() => null) : null;
    if (payload && allowed(payload, request)) return fetch(request);
    const to = new URL('https://heretic.tech/clearance');
    to.searchParams.set('sitekey', SITE_KEY);
    to.searchParams.set('return', url.toString());
    return Response.redirect(to.toString(), 302);
  },
};
```

`allowed` is [your rule](#your-rule).

## Background

Load `guard.js` with `data-clearance="true"`. The embed runs without interaction and `data-clearance-callback` receives the JWT. Post it to your own endpoint, which verifies it and sets the cookie for the next request.

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

<div class="heretic-guard"
     data-sitekey="hrtc_live_0123456789abcdef01234567"
     data-clearance="true"
     data-clearance-callback="onClearance"></div>

<script>
  function onClearance(jwt) {
    fetch('/heretic/clearance', { method: 'POST', body: jwt, credentials: 'same-origin' });
  }
</script>
```

`heretic.getClearance(id)` reads the JWT of a widget rendered with the [script API](/integration/challenge-widget#script-api).

## Guarded action

A form keeps the five-minute proof and [siteverify](/api/siteverify-endpoint). Apply the same gate check to the form post, so a cleared visitor is not probed again there. Configure the widget on the [challenge widget](/integration/challenge-widget) page.

## What the token contains

Header: `alg` is `EdDSA`, `typ` is `JWT`, and `kid` is `site_<id>/<version>`.

```json theme={"dark"}
{
  "iss": "https://heretic.tech",
  "aud": "shop.example",
  "sub": "0123456789abcdef0123456789abcdef",
  "jti": "<32 hex, random>",
  "iat": 1757300000,
  "exp": 1757386400,
  "heretic": {
    "v": 1,
    "site_id": "site_…",
    "site_key": "hrtc_live_…",
    "verdict": "uncontradicted",
    "concealed": false,
    "challenged": false,
    "outcome": null,
    "probe": {
      "request_id": "0123456789abcdef0123456789abcdef",
      "measured_at": 1757299990000,
      "client_ip": "203.0.113.7",
      "network": { "asn": 133481, "country": "TH", "class": "access" },
      "identity": { "version": 3, "instance": "<64 hex>", "machine": "<64 hex>" }
    },
    "challenge": null
  }
}
```

| Field                                              | Meaning                                                                                                                |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `iss`                                              | Always `https://heretic.tech`.                                                                                         |
| `aud`                                              | The site's registered domain. A gate on `www.` or a subdomain compares against the registered domain.                  |
| `sub`                                              | The probe `request_id`, or the challenge id when no probe was read.                                                    |
| `jti`                                              | Random, 32 hex.                                                                                                        |
| `iat`, `exp`                                       | Seconds. `exp` is `iat` plus the site's TTL. Your gate may require a shorter age from `iat`.                           |
| `heretic.v`                                        | `1`.                                                                                                                   |
| `heretic.site_id`, `heretic.site_key`              | The site the token was issued for.                                                                                     |
| `heretic.verdict`                                  | `uncontradicted`, `contradicted`, or `refused`. `refused` with `probe: null` when the widget could not read the probe. |
| `heretic.concealed`                                | `true` when the probe reported any concealment indicator. It never changes the verdict.                                |
| `heretic.challenged`, `heretic.outcome`            | `true` and `passed`, `contradicted`, or `not_completed` when a ceremony ran. Otherwise `false` and `null`.             |
| `heretic.probe.request_id`                         | Keys [`GET /v1/verdict`](/api/verdict-endpoint).                                                                       |
| `heretic.probe.measured_at`                        | Milliseconds.                                                                                                          |
| `heretic.probe.client_ip`, `heretic.probe.network` | The measured address, and its `asn`, `country`, and `class`.                                                           |
| `heretic.probe.identity`                           | `version`, `instance`, and `machine`. Each hash is 64 hex or `null`.                                                   |
| `heretic.challenge`                                | The ceremony when one ran. Otherwise `null`.                                                                           |

When a ceremony ran:

```json theme={"dark"}
"challenge": {
  "id": "chg_…",
  "completed_at": 1757300100000,
  "device": {
    "request_id": "<32 hex>",
    "client_ip": "198.51.100.9",
    "network": { "asn": 5617, "country": "PL", "class": "access" },
    "identity": { "version": 3, "scope": "challenge-tenant-v1", "instance": "<64 hex>", "machine": "<64 hex>" }
  }
}
```

`device` is the device the ceremony cleared: the desktop in a QR hand-off, otherwise the phone. Its identity hashes are in the challenge scope. Compare them only with other [ceremony devices](/api/challenge-read#the-devices), never with `probe.identity`.

## Your rule

Verify the signature, `iss`, `aud`, and `exp`, then apply your own rule. This example accepts an uncontradicted probe without concealment, or a passed ceremony, and requires the live request to come from the measured address and network. It is this site's rule, not a Heretic threshold.

```js theme={"dark"}
function allowed(payload, request) {
  const { verdict, concealed, challenged, outcome, probe } = payload.heretic;
  if (challenged ? outcome !== 'passed' : verdict !== 'uncontradicted' || concealed) return false;
  if (!probe) return false;
  return probe.client_ip === request.headers.get('cf-connecting-ip')
    && probe.network.asn === request.cf.asn;
}
```

## Facts

The hosted page is the only placement that gates the first request. It costs one wait of a few seconds per visitor per TTL.

A client that does not run the collector is never cleared. That includes search crawlers. What happens to an uncleared request is your rule.

Findings and concealment indicators are not in the token. The visitor can read the token. Read them with your secret key at [`GET /v1/verdict/{request_id}`](/api/verdict-endpoint).

## Dashboard settings

On the site's panel in the [dashboard](/dashboard/dashboard-and-keys#clearance):

| Setting             | Meaning                                                                                                                                                                           |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| TTL                 | Token lifetime. Default 86,400 s. 300 s to 30 days.                                                                                                                               |
| Escalation          | Which probe results run a ceremony on the hosted page. `contradicted` (default), `refused`, `always`, `never`, as for [`data-escalate`](/integration/challenge-widget#configure). |
| Challenge concealed | Also runs a ceremony when the probe reports concealment indicators. Default off.                                                                                                  |
| Rotate signing key  | Issues a new key version. See [rotation](/api/clearance-keys#rotation).                                                                                                           |

## Verifiers

The [server examples](https://heretic.tech/downloads/heretic-server-examples.zip) include `node/clearance.mjs`, `python/clearance.py`, `php/Clearance.php`, and a Cloudflare Worker gate at `examples/cloudflare-worker/clearance-gate.js`.


## Related topics

- [Dashboard, sites, and keys](/dashboard/dashboard-and-keys.md)
- [Clearance keys](/api/clearance-keys.md)
- [Challenge widget](/integration/challenge-widget.md)
- [Pricing](/reference/pricing.md)
