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

# Model providers

> Verify the authorization token at your inference API before you run a request that names a protected talent.

<Note>
  Protocol integration design. The current sandbox issues test authorization tokens only.
  Production enforcement, the name index and revocation feed described below are not available
  through the Talent API sandbox.
  Start with the [runnable quickstart](/quickstart) and [sandbox support matrix](/sandbox).
</Note>

Platforms that call your inference API send an authorization token with every request for a
named talent. The token is Reflections' signed `allow` for exactly that use: which talent, which
license revision, which model, how many generations and until when. Verify it before your model
runs. Verification is local: a signature check against a cached key set and a few comparisons,
with no call to Reflections on the request path.

## Where the token comes from

The platform runs a check, receives `allow` with `authorization.token`, and passes the token to
you with the job. Document where your API accepts it, such as a `talent_authorization` field or
header. Require it whenever a request names a protected talent: customers can mark those requests,
and with a Reflections account you can also match prompts against the signed
[name index](/api-reference/talent/download-the-name-index) and require a token on a match.

| Claim | What it tells you |
| - | - |
| `aud` | The platform the token was issued to, as a Reflections account ID |
| `sub` | The licensee whose license the use relies on |
| `jti` | The check ID; one count of generations per `jti` |
| `exp` | When the token stops working, at most 15 minutes after `iat` |
| `tlp.execution.model` | The model the platform said would run |
| `tlp.max_generations` | How many outputs the token covers in total |
| `tlp.obligations` | The obligation types the platform must meet |

[Authorization token](/api-reference/formats/authorization-token) lists every claim.

## Map each customer to a Reflections account

A token names its platform in `aud`, so you need to know which Reflections account each of your
customers is. Ask each customer for its account ID when it sets up access; it reads it from
[`GET /v1/account`](/api-reference/account/retrieve-your-account). Accept a token only when `aud`
is the account of the customer making the request. A token issued to a different platform then
fails, even if it was copied from a valid request.

## Verify before you run

Reject the request when any of these fails:

| Check | Reject when |
| - | - |
| Signature | The token is not signed by a key in the JWKS |
| `typ` and `iss` | `typ` is not `tlp-auth+jwt`, or `iss` is not `https://app.reflections.ai` |
| Audience | `aud` is not the calling customer's account |
| Time | `exp` has passed, or `iat` is in the future, allowing at most 60 seconds of leeway |
| Version | `tlp.ver` does not have major version `1` |
| Obligations | `tlp.obligations` holds a type you do not handle |
| Model | `tlp.execution.model` is not the model about to run |
| Count | This job would take the total for its `jti` past `tlp.max_generations` |

A job that returns four images uses four generations. Reserve them before the job starts, with an
atomic increment in a store shared by every instance of your API, and keep the counter until 60
seconds after `exp`.

<CodeGroup>
  ```typescript TypeScript theme={null}
  // npm install jose
  import { createRemoteJWKSet, jwtVerify } from "jose";

  // The JWKS is served with max-age=3600, and a live key is published 30 days before it signs, so
  // refetch for an unknown kid at most once an hour (TLP-TOK-9).
  const JWKS = createRemoteJWKSet(new URL("https://app.reflections.ai/.well-known/jwks.json"), {
    cacheMaxAge: 3_600_000,
    cooldownDuration: 3_600_000,
  });
  // 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",
  ]);

  type Tlp = {
    ver: string;
    decision: string;
    max_generations: number;
    execution: { provider: string; model: string };
    obligations: string[];
  };

  // reserve(jti, n, max, exp) atomically adds n to the count for jti and returns false
  // if that would exceed max. Implement it on your shared datastore.
  type Reserve = (jti: string, n: number, max: number, exp: number) => Promise<boolean>;

  export async function authorizeJob(
    token: string,
    customerAccount: string, // the acct_ ID stored for the calling customer
    model: string, // the model this job runs
    outputs: number, // outputs this job produces
    reserve: Reserve,
  ) {
    const { payload } = await jwtVerify<{ tlp: Tlp }>(token, JWKS, {
      issuer: "https://app.reflections.ai",
      audience: customerAccount,
      typ: "tlp-auth+jwt",
      algorithms: ["ES256"],
      clockTolerance: 60,
      maxTokenAge: "15m", // also rejects an iat in the future
      requiredClaims: ["exp", "iat", "jti", "sub"],
    });
    const { tlp } = payload;
    if (tlp.ver.split(".")[0] !== "1") throw new Error("unsupported_version");
    if (!tlp.obligations.every((type) => OBLIGATIONS.has(type))) throw new Error("unknown_obligation");
    if (tlp.decision !== "allow") throw new Error("not_allowed");
    if (tlp.execution.model !== model) throw new Error("model_mismatch");
    if (!(await reserve(payload.jti!, outputs, payload.tlp.max_generations, payload.exp!))) {
      throw new Error("max_generations_reached");
    }
    return payload;
  }
  ```

  ```python Python theme={null}
  from typing import Callable
  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]

  # reserve(jti, n, max, exp) atomically adds n to the count for jti and returns False
  # if that would exceed max. Implement it on your shared datastore.
  Reserve = Callable[[str, int, int, int], bool]

  def authorize_job(token: str, customer_account: str, model: str, outputs: int, reserve: Reserve) -> dict:
      if jwt.get_unverified_header(token).get("typ") != "tlp-auth+jwt":
          raise PermissionError("wrong_type")
      claims = jwt.decode(
          token,
          signing_key(token),
          algorithms=["ES256"],
          audience=customer_account,
          issuer="https://app.reflections.ai",
          leeway=60,
          options={"require": ["exp", "iat", "jti", "sub"]},  # also rejects an iat in the future
      )
      tlp = claims["tlp"]
      if str(tlp["ver"]).split(".")[0] != "1":
          raise PermissionError("unsupported_version")
      if not set(tlp["obligations"]) <= OBLIGATIONS:
          raise PermissionError("unknown_obligation")
      if tlp["decision"] != "allow":
          raise PermissionError("not_allowed")
      if tlp["execution"]["model"] != model:
          raise PermissionError("model_mismatch")
      if not reserve(claims["jti"], outputs, tlp["max_generations"], claims["exp"]):
          raise PermissionError("max_generations_reached")
      return claims
  ```
</CodeGroup>

When you reject, return an error in your API's own format that names the failed check: expired,
wrong audience, model mismatch, count reached or invalid signature. The platform's fix is the same
in every case: run a new check. Never substitute another model for the one the token names.

## Caching

Cache the key set, not decisions.

* Cache the JWKS for its `Cache-Control` max-age. When a token names a `kid` you do not have,
  refetch at most once per max-age, so forged `kid` values cannot make you fetch on every request.
  A new live key is published 30 days before it signs anything, retired keys stay published, and a
  `kid` is never reused. Sandbox `rfl-test-` keys can rotate without that notice.
* If the refetch fails or the `kid` is still unknown, reject. A token you cannot verify is not an
  allow.
* Never cache a deny as an allow. A missing token, a rejected token or a failed key fetch is never
  stored as a pass for a later request, and an earlier valid token from the same customer says
  nothing about the next one.
* A verified token is good only for itself, until its `exp`, within its count.

Because verification needs no network call once the JWKS is cached, a Reflections outage does
not stop you from verifying tokens already issued. It does stop new tokens, and a request for a
named talent without one is rejected.

## Test tokens

Tokens issued in test mode are signed with test keys, whose `kid` starts with `rfl-test-`, such as
`rfl-test-2026-10-docs3`. Accept them only in your own test environment for integration tests, and
reject them in production. The [conformance plan](/production/conformance) describes proposed
cases; no hosted suite is available.

## Session tokens

Realtime models receive a session token instead: `typ` `tlp-session+jwt`, the same `iss`, `aud`
and `sub`, a `jti` that is the session ID, and an `exp` 5 minutes after issue. The platform renews
it every 60 seconds and sends you the new one. Verify each token the same way, without the count,
check that `tlp.session` is the session you are running, and stop generating speech for the
session when its current token expires without a replacement.
See [Session token](/api-reference/formats/session-token).

## Copies you hold

When a talent's voice, adapter or persona exists on your service, its facet lists you in
`provider_bindings`, with your `external_id` and `kind` (`voice`, `adapter` or `persona`).
Bindings are shown to you and to licensees with material access. When a talent withdraws the facet
or revokes a license, the platform that created the copy is told to delete it and asks you to.
Delete it, and give the platform the deletion time; it reports it to Reflections in the
revocation acknowledgement. If you hold a Reflections account, subscribe to `facet.updated` to see
binding changes.

## What can go wrong

| Problem | Cause | What to do |
| - | - | - |
| Valid tokens fail on `aud` | You compare with your own account, or the customer's account ID is out of date | Compare with the calling customer's stored account ID |
| Tokens fail on `exp` | Clock drift on your hosts | Keep clocks synchronized; leeway is at most 60 seconds |
| The same token runs twice | Counts are kept per instance | Count in a shared store with an atomic increment |
| Every token fails after a key rotation | Your JWKS cache ignores unknown `kid` values | Refetch once on an unknown `kid` |

## Related

<Columns cols={2}>
  <Card title="Tokens" icon="key" href="/standard/tokens">
    Token formats and verification rules in the Standard.
  </Card>

  <Card title="Agent builders" icon="robot" href="/guides/agent-builders">
    The same verification inside an agent's tool.
  </Card>

  <Card title="Security" icon="shield-halved" href="/production/security">
    Key handling, token verification and webhook signing.
  </Card>

  <Card title="Discovery" icon="compass" href="/api-reference/formats/discovery">
    The discovery document and the JWKS.
  </Card>
</Columns>


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