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

> ## Agent Instructions
> The OpenAPI document at /api-reference/openapi.yaml defines the target contract. Its x-reflections-availability field and each operation's availability notice identify supported test and live forms. Do not infer implementation from a schema or code sample. Live keys support account details, reads of the licenses the calling account holds as licensee or issued as talent (GET /v1/licenses?role=issuer), and GET /v1/generations?license=… for the state of outputs under such a license (summaries only: no prompt, reference, bytes or metadata). Live image checks, submissions and receipts are built but not activated: until activation, a live POST /v1/checks or /v1/generations returns 403 live_not_activated, which must not be retried. Discovery and JWKS are at the origin root (https://app.reflections.ai/.well-known/...), not under /v1.
> Three rules hold everywhere: a decision is never an error (deny and escalate are 200 responses, and a live check still evaluating is a 202 with status checking); generate nothing for a named talent without an allow; deliver nothing until the generation is allow.
> The sandbox accepts personal rk_test_ keys. Persist a separate Idempotency-Key and body for each logical /v1 POST; retry an unknown outcome with the same key and body. OAuth and Talent API MCP remain planned.

# Authorization token

> The signed token an allowed check returns, its claims, and how a generation tool verifies it before it generates.

An `allow` from [`POST /v1/checks`](/api-reference/checks/create-a-check) carries
`authorization.token`. Pass it to the tool or model provider that runs the generation. The tool
verifies it offline against the published keys, and generates only what the token covers.

The token is a JWS in compact serialization, signed with ES256 (ECDSA on P-256 with SHA-256).

## Header

```json theme={null}
{
  "alg": "ES256",
  "kid": "rfl-2026-10-docs3",
  "typ": "tlp-auth+jwt"
}
```

| Parameter | Value |
| - | - |
| `alg` | Always `ES256`. Reject any other value. |
| `kid` | The signing key in the [JWKS](/api-reference/formats/discovery#jwks). Live keys look like `rfl-2026-10-docs3`, test keys like `rfl-test-2026-10-docs3`. |
| `typ` | Always `tlp-auth+jwt`. A session token or a receipt has a different `typ` and is not an authorization. |

## Claims

| Claim | Type | Value |
| - | - | - |
| `iss` | string | `https://app.reflections.ai` |
| `aud` | string | The `acct_` ID of the platform that runs the generation. |
| `sub` | string | The `acct_` ID of the licensee whose license the check relied on. Equal to `aud` when a platform holds the license itself. |
| `jti` | string | The check ID, `chk_…`. |
| `iat` | number | Issue time, in Unix seconds. |
| `exp` | number | Expiry, in Unix seconds. At most 15 minutes after `iat`, and equal to the check's `authorization.expires_at`. |
| `tlp.ver` | string | The protocol version, `"1.0"`. |
| `tlp.decision` | string | Always `"allow"`. |
| `tlp.talent` | string array | The `tal_` IDs the check covered. |
| `tlp.facets` | string array | The `fct_` IDs the check covered. |
| `tlp.licenses` | object array | The licenses the check relied on: one entry per talent whose license applied, in the order of `tlp.talent`. Each entry has `talent` (`tal_`), `license` (`lic_`), `revision` (the `lrv_` ID of the signed revision) and `terms_digest` (that revision's `terms_digest`, `sha256:` and hex). Empty when no license applied, as when each talent's [posture](/concepts/talent-and-facets#posture) declares the use `allowed`. |
| `tlp.request_digest` | string | The digest of the request the check decided. See [Request digest](#request-digest). |
| `tlp.use` | object | The check's `use`: `modality`, `kind`, `purpose`, `commercial`, `channels`, `territories`. |
| `tlp.max_generations` | integer | How many generations this token covers. |
| `tlp.execution` | object | The `provider` and `model` the check was made for. |
| `tlp.eu` | string | An HMAC of the end user's ID, in hex. It identifies the end user without revealing the ID. |
| `tlp.obligations` | string array | The obligation types the platform must meet, such as `generation_check`. |

The full obligations, with their instructions and disclosure text, are on the check. The token
lists only their types, so a tool can tell, for example, that a `generation_check` follows.

## Example payload

The token from Ridgeline's image check, run by Northlight for one end user:

```json theme={null}
{
  "iss": "https://app.reflections.ai",
  "aud": "acct_7p4r6s9t0wxyhk5zba2q8v1m3n",
  "sub": "acct_2q8v1m3n7p4r6s9t0wxyhk5zba",
  "jti": "chk_3m7p4r6s9t0wxyhk5zba2q8v1n",
  "iat": 1792260000,
  "exp": 1792260900,
  "tlp": {
    "ver": "1.0",
    "decision": "allow",
    "talent": ["tal_9t0wxyhk5zba2q8v1m3n7p4r6s"],
    "facets": ["fct_5zba2q8v1m3n7p4r6s9t0wxyhk"],
    "licenses": [
      {
        "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
        "license": "lic_k5zba2q8v1m3n7p4r6s9t0wxyh",
        "revision": "lrv_p4r6s9t0wxyhk5zba2q8v1m3n7",
        "terms_digest": "sha256:39f18c8bef02b5226b9feaa92e526ed30b81b91650d8a10b503281b169f44431"
      }
    ],
    "request_digest": "sha256:bf94817766d56d92211d12f5ff51a3dc1a6d5cb4be2877417b08251fe38c386a",
    "use": {
      "modality": "image",
      "kind": "depiction",
      "purpose": "advertising",
      "commercial": true,
      "channels": ["social", "web"],
      "territories": ["US", "CA"]
    },
    "max_generations": 4,
    "execution": { "provider": "example-image-provider", "model": "image-model-3" },
    "eu": "3f9c1a7e5b2d8f4061c3e9a7b5d2f8016e4c9a3b7d1f5e2c8a6b4d0f9e3c7a15",
    "obligations": [
      "generation_check",
      "hold_until_release",
      "draft_watermark",
      "generation_instructions",
      "disclosure",
      "c2pa_manifest",
      "no_training",
      "usage_report"
    ]
  }
}
```

`exp` is `2026-10-17T18:15:00Z`, fifteen minutes after `iat`.

## Verify a token

A tool verifies the token before it runs anything. Reject the request if any step fails.

<Steps>
  <Step title="Check the header">
    `alg` is `ES256` and `typ` is `tlp-auth+jwt`. In live mode, reject a `kid` that starts with
    `rfl-test-`.
  </Step>

  <Step title="Check the signature">
    Find the key whose `kid` matches in the
    [JWKS](https://app.reflections.ai/.well-known/jwks.json). Cache the JWKS for its
    `Cache-Control` max-age. When a token names an unknown `kid`, refetch it, at most once per
    max-age. Verify the ES256 signature over `<header>.<payload>`.
  </Step>

  <Step title="Check who it is for">
    `iss` is `https://app.reflections.ai`, and `aud` is the `acct_` ID of the platform you serve. A
    token issued to another platform fails here.
  </Step>

  <Step title="Check the time">
    The current time is no later than `exp` plus 60 seconds of leeway, and no earlier than `iat`
    minus 60 seconds.
  </Step>

  <Step title="Check what it covers">
    `tlp.ver` has major version `1`, `tlp.decision` is `allow`, `tlp.execution.model` is the model
    about to run, and every type in `tlp.obligations` is one your tool handles.
  </Step>

  <Step title="Count generations">
    Keep a count per `jti` in a store every instance shares, until 60 seconds after `exp`. Refuse a
    generation once the count reaches `tlp.max_generations`.
  </Step>
</Steps>

These steps do not need the license. A party that can read it, such as the licensee or a
connected platform, can also confirm the signed terms: for each entry in `tlp.licenses`, read the
revision it names with
[`GET /v1/licenses/{license_id}/revisions/{revision_id}`](/api-reference/licenses/retrieve-a-license-revision)
and compare that revision's `terms_digest` with the entry's.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { createPublicKey, verify } from "node:crypto";

  type Jwk = { kid: string; kty: string; crv: string; x: string; y: string };
  let jwks = { keys: [] as Jwk[], expiresAt: 0, maxAge: 300 };
  let refetchAfter = 0;

  // The obligation types of TLP 1.0. Reject a token that carries one you do not handle.
  const OBLIGATIONS = new Set([
    "generation_check", "hold_until_release", "draft_watermark", "generation_instructions",
    "disclosure", "attribution", "c2pa_manifest", "no_training", "delete_material", "usage_report",
  ]);

  async function load() {
    const res = await fetch("https://app.reflections.ai/.well-known/jwks.json");
    if (!res.ok) throw new Error(`JWKS fetch failed: ${res.status}`);
    const maxAge = Number(/max-age=(\d+)/.exec(res.headers.get("Cache-Control") ?? "")?.[1] ?? 300);
    jwks = { keys: (await res.json()).keys, expiresAt: Date.now() + maxAge * 1000, maxAge };
  }

  async function signingKey(kid: string): Promise<Jwk | undefined> {
    if (Date.now() >= jwks.expiresAt) await load();
    let key = jwks.keys.find((k) => k.kid === kid);
    if (!key && Date.now() >= refetchAfter) {
      refetchAfter = Date.now() + jwks.maxAge * 1000; // at most one refetch per max-age (TLP-TOK-9)
      await load();
      key = jwks.keys.find((k) => k.kid === kid);
    }
    return key;
  }

  export async function verifyAuthorization(token: string, platform: string, model: string, live: boolean) {
    const [h, p, s] = token.split(".");
    if (!h || !p || !s) throw new Error("malformed token");
    const header = JSON.parse(Buffer.from(h, "base64url").toString("utf8"));
    if (header.alg !== "ES256" || header.typ !== "tlp-auth+jwt") throw new Error("not an authorization token");
    if (live && String(header.kid).startsWith("rfl-test-")) throw new Error("test token in live mode");

    const jwk = await signingKey(header.kid);
    if (!jwk) throw new Error("unknown kid");
    const key = createPublicKey({ key: jwk, format: "jwk" });
    const signed = Buffer.from(`${h}.${p}`);
    if (!verify("sha256", signed, { key, dsaEncoding: "ieee-p1363" }, Buffer.from(s, "base64url"))) {
      throw new Error("bad signature");
    }

    const claims = JSON.parse(Buffer.from(p, "base64url").toString("utf8"));
    if (claims.iss !== "https://app.reflections.ai") throw new Error("wrong issuer");
    if (claims.aud !== platform) throw new Error("token is for another platform");
    const now = Date.now() / 1000;
    if (now > claims.exp + 60) throw new Error("token expired");
    if (now < claims.iat - 60) throw new Error("token issued in the future");
    if (String(claims.tlp?.ver).split(".")[0] !== "1") throw new Error("unsupported protocol version");
    if (claims.tlp.decision !== "allow") throw new Error("not an allow");
    if (claims.tlp.execution?.model !== model) throw new Error("token is for another model");
    if (!claims.tlp.obligations.every((t: string) => OBLIGATIONS.has(t))) throw new Error("unknown obligation");
    return claims; // Then count generations for claims.jti against claims.tlp.max_generations.
  }
  ```

  ```python Python theme={null}
  import time
  import jwt  # pip install "pyjwt[crypto]"

  JWKS = jwt.PyJWKClient("https://app.reflections.ai/.well-known/jwks.json", lifespan=3600)
  refetch_after = 0.0
  # The obligation types of TLP 1.0. Reject a token that carries one you do not handle.
  OBLIGATIONS = {
      "generation_check", "hold_until_release", "draft_watermark", "generation_instructions",
      "disclosure", "attribution", "c2pa_manifest", "no_training", "delete_material", "usage_report",
  }

  def signing_key(token: str):
      """The token's key. Refetches the JWKS for an unknown kid at most once an hour (TLP-TOK-9)."""
      global refetch_after
      kid = jwt.get_unverified_header(token).get("kid")
      keys = {k.key_id: k.key for k in JWKS.get_signing_keys()}
      if kid not in keys and time.time() >= refetch_after:
          refetch_after = time.time() + 3600
          keys = {k.key_id: k.key for k in JWKS.get_signing_keys(refresh=True)}
      if kid not in keys:
          raise PermissionError("unknown_kid")
      return keys[kid]

  def verify_authorization(token: str, platform: str, model: str, live: bool) -> dict:
      header = jwt.get_unverified_header(token)
      if header.get("alg") != "ES256" or header.get("typ") != "tlp-auth+jwt":
          raise ValueError("not an authorization token")
      if live and str(header.get("kid", "")).startswith("rfl-test-"):
          raise ValueError("test token in live mode")

      claims = jwt.decode(
          token,
          signing_key(token),
          algorithms=["ES256"],
          audience=platform,
          issuer="https://app.reflections.ai",
          leeway=60,
          options={"require": ["iss", "aud", "exp", "iat", "jti"]},  # also rejects an iat in the future
      )
      tlp = claims["tlp"]
      if str(tlp["ver"]).split(".")[0] != "1":
          raise ValueError("unsupported protocol version")
      if tlp["decision"] != "allow":
          raise ValueError("not an allow")
      if tlp["execution"]["model"] != model:
          raise ValueError("token is for another model")
      if not set(tlp["obligations"]) <= OBLIGATIONS:
          raise ValueError("unknown obligation")
      return claims  # Then count generations for claims["jti"] against claims["tlp"]["max_generations"].
  ```
</CodeGroup>

## Request digest

`tlp.request_digest` binds the token to the exact request the check decided. If the tool receives
the inputs, it can recompute the digest and refuse a request that differs. The token does not show
whether the check asked for `reuse`, and under a reuse window later requests differ by design, so
compare digests only when the platform tells you the check had no `reuse`.

The digest is `sha256:` and the lowercase hex SHA-256 of the `tlp-json-1` canonical form of this
object, shown here before canonicalization:

```json theme={null}
{
  "inputs": [
    { "role": "prompt", "text": "Maya Okafor holding our new trail shoe on a rocky ridge at sunrise" },
    { "role": "reference", "sha256": "5e0b7c2d9f4a1e6b3c8d0f2a4e6b8c0d2f4a6e8b0c2d4f6a8e0b2c4d6f8a0b2c", "media_type": "image/jpeg" }
  ],
  "use": { "modality": "image", "kind": "depiction", "purpose": "advertising", "commercial": true, "channels": ["social", "web"], "territories": ["US", "CA"] },
  "execution": { "provider": "example-image-provider", "model": "image-model-3" }
}
```

* `inputs` are in the order sent. A text input keeps `role` and `text`. A media input is replaced by
  `role`, `sha256` and `media_type`, whether it was sent as a file, a URL or a digest.
* `execution` keeps only `provider` and `model`.
* `tlp-json-1`: UTF-8 JSON, object keys sorted by Unicode code point, no insignificant whitespace,
  numbers in their shortest round-trip form, no duplicate keys.

See [Binding the request](/build/binding-the-request) for where each input comes from.

## Related

<Columns cols={2}>
  <Card title="Agent builders" icon="robot" href="/guides/agent-builders">
    Pass the token with each tool call, and verify it in the tool.
  </Card>

  <Card title="Model providers" icon="code" href="/guides/model-providers">
    Verify tokens at your inference API.
  </Card>

  <Card title="Session token" icon="microphone" href="/api-reference/formats/session-token">
    The short-lived token for live sessions.
  </Card>

  <Card title="Tokens in the Standard" icon="book" href="/standard/tokens">
    The normative token rules.
  </Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.