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

> Fetch the public keys that verify a site's clearance tokens.

Public. No secret is required. Verify a [clearance](/integration/clearance) against these keys on your server or edge.

```json theme={"dark"}
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "kid": "site_0123456789abcdef01234567/1",
      "x": "<base64url>",
      "alg": "EdDSA",
      "use": "sig"
    }
  ]
}
```

`kid` matches the token header. `x` is the raw Ed25519 public key. The response carries `cache-control: public, max-age=300`. Cache keys by `kid`.

## Status codes

| Code  | Cause                                                 |
| ----- | ----------------------------------------------------- |
| `404` | Unknown or revoked site key.                          |
| `403` | `site-unverified`. The site's domain is not verified. |

## Rotation

**Rotate signing key** on the site's dashboard panel increments the key version. New tokens carry the new `kid`. The response lists the current and the previous version, so a token signed before rotation verifies until it expires.

## Verify

Node with `jose`:

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

const JWKS = createRemoteJWKSet(new URL('https://heretic.tech/v1/clearance/keys/hrtc_live_…'));

const { payload } = await jwtVerify(jwt, JWKS, {
  issuer: 'https://heretic.tech', audience: 'shop.example', algorithms: ['EdDSA'],
});
```

Python with PyJWT and `cryptography`:

```python theme={"dark"}
import jwt

jwks = jwt.PyJWKClient("https://heretic.tech/v1/clearance/keys/hrtc_live_…")

key = jwks.get_signing_key_from_jwt(token)
payload = jwt.decode(token, key.key, algorithms=["EdDSA"],
                     audience="shop.example", issuer="https://heretic.tech")
```

PHP with `firebase/php-jwt`. The library checks `exp`; check `iss` and `aud` yourself.

```php theme={"dark"}
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;

$jwks = json_decode(file_get_contents('https://heretic.tech/v1/clearance/keys/hrtc_live_…'), true);
$payload = JWT::decode($jwt, JWK::parseKeySet($jwks, 'EdDSA'));
if ($payload->iss !== 'https://heretic.tech' || $payload->aud !== 'shop.example') {
    // refuse
}
```

Cloudflare Workers with WebCrypto and no library:

```js theme={"dark"}
const b64u = (s) => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
const json = (s) => JSON.parse(new TextDecoder().decode(b64u(s)));

async function verifyClearance(jwt, siteKey, domain) {
  const [h, p, s] = jwt.split('.');
  const header = json(h);
  if (header.alg !== 'EdDSA') return null;
  const { keys } = await (await fetch(`https://heretic.tech/v1/clearance/keys/${siteKey}`)).json();
  const jwk = keys.find((k) => k.kid === header.kid);
  if (!jwk) return null;
  const key = await crypto.subtle.importKey('jwk', { kty: jwk.kty, crv: jwk.crv, x: jwk.x }, { name: 'Ed25519' }, false, ['verify']);
  const ok = await crypto.subtle.verify('Ed25519', key, b64u(s), new TextEncoder().encode(`${h}.${p}`));
  if (!ok) return null;
  const payload = json(p);
  const now = Math.floor(Date.now() / 1000);
  if (payload.iss !== 'https://heretic.tech' || payload.aud !== domain || payload.exp <= now) return null;
  return payload;
}
```

Then apply [your rule](/integration/clearance#your-rule).


## Related topics

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