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

# Licenses

> Licenses, their signed revisions and digests, and the terms a license can carry.

<Note>
  Live keys can read the licenses the calling account holds as licensee or issued as talent, and
  their current signed revision digests. Sandbox licenses and revisions are fictional fixtures. The broader lifecycle and
  revision contract below remains planned for live mode.
  Start with the [runnable quickstart](/quickstart) and [sandbox support matrix](/sandbox).
</Note>

A **license** is a signed agreement between a talent and a licensee that says what may be made with
the talent's facets. Every check, generation and session is decided against one license revision,
and every decision names it. This page covers what a license holds, how it changes, and how to
confirm you hold the document the talent signed.

## Live license reads

[`GET /v1/licenses`](/api-reference/licenses/list-licenses) and
[`GET /v1/licenses/{license_id}`](/api-reference/licenses/retrieve-a-license) return real licenses
when the calling account is their sole licensee, or when it issued them as talent:
`GET /v1/licenses?role=issuer` lists those. In test mode, `role=issuer` returns an empty list.
Delegated connections and representative access remain planned. Use the IDs returned by a list
call; any other license is not found.

The issuing talent can also follow what the licensee made under her license with
[`GET /v1/generations?license=…`](/api-reference/generations/list-generations): each output's
status, decision, receipt and release status, never its prompt, references, bytes or metadata.
Add `release_status=awaiting_release` to list only the finals waiting for her release. Releasing
or declining a final through the API is planned; she decides in the Talent Portal.

`current_revision`, `revision_number` and `terms_digest` identify the current signed revision.
The digest covers the full signed terms, not the partial `scope` returned here. Live revision
retrieval is unavailable, so this response cannot reconstruct the signed document.

Live `scope` returns `window`, `required_attributes`, `release_policy` and `final_deliverables`, plus
`external_creation` only when that grant is explicitly signed.
Signed channel and territory labels do not safely map to protocol enums and are omitted. Omitted
fields grant nothing. No private reference IDs or source media are returned.

`external_creation: "images.v1"` records prospective permission for still images made in the
licensee's tools with inputs it lawfully holds, subject to request and output checks and mandatory
talent release. It does not permit exporting private references. Older licenses without this
signed term do not acquire it from `make`, a creation profile or `offPlatformProduction`. This is
reader support only: no issuance route can add the term yet, and live checks, output submission
and receipts remain unavailable.

An accepted license is `scheduled` before its window, `active` within it and `expired` at or after
its end. Issued licenses are `pending_acceptance`; `declined`, `revoked` and `void` are preserved.
These reads do not run a precheck, evaluate an output or issue a live receipt.

## What a license holds

| Field | What it holds |
| - | - |
| `talent`, `licensee` | The two parties |
| `issued_by` | Who signed for the talent: the talent, or a `representative` with `issue_licenses` |
| `handle` | A six-character name people can read aloud, such as `K7Q4M2` |
| `status` | Where the license is in its life, below |
| `current_revision`, `revision_number` | The revision in force |
| `terms_digest` | The digest of the current revision's signed terms |
| `scope` | The current terms in normalized form |
| `pending_revision` | A new revision the talent's side signed, waiting for the licensee |
| `acceptance_url` | The hosted page where the licensee reads and accepts the license or a pending revision |

When several licenses could cover a use, a check uses the narrowest one that allows it. Name one
with `license` on the check to choose.

## Statuses

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending_acceptance: talent's side signs
    pending_acceptance --> declined: licensee declines
    pending_acceptance --> scheduled: accepted before the window
    pending_acceptance --> active: accepted in the window
    scheduled --> active: window starts
    active --> suspended: paused
    suspended --> active: resumed
    active --> expired: window ends
    suspended --> expired: window ends
    expired --> scheduled: renewal accepted
    expired --> active: renewal accepted
    scheduled --> revoked: revoked
    active --> revoked: revoked
    suspended --> revoked: revoked
    declined --> [*]
    expired --> [*]
    revoked --> [*]
```

| Status | Meaning | A check under it |
| - | - | - |
| `pending_acceptance` | Signed on the talent's side, waiting for the licensee | `deny`, `license_pending_acceptance` |
| `scheduled` | Accepted, window not started | `deny`, `window_not_started` |
| `active` | In force | Decided on its terms |
| `suspended` | Paused by the talent or a representative | `deny`, `license_suspended`; held outputs stay held |
| `expired` | The window ended. A renewal the licensee accepts makes it `scheduled` or `active` again. | `deny`, `window_ended` |
| `declined` | The licensee declined it | Never used |
| `revoked` | Ended by the talent or a representative | `deny`, `license_revoked` |
| `void` | Voided in the issuing system | Never used; the sandbox status filter does not accept this status |

The planned lifecycle sends webhooks: [`license.issued`](/api-reference/webhooks/license-issued),
[`license.accepted`](/api-reference/webhooks/license-accepted),
[`license.suspended`](/api-reference/webhooks/license-suspended),
[`license.resumed`](/api-reference/webhooks/license-resumed),
[`license.expired`](/api-reference/webhooks/license-expired),
[`license.declined`](/api-reference/webhooks/license-declined) and
[`license.revoked`](/api-reference/webhooks/license-revoked).

## Revisions

Each signed version of a license is a **license revision**, and a revision's terms never change.
Changing a license means signing a new revision: `source_kind` is `issue`, `change_request`,
`amend` or `renew`. A revision carries `signatures`, each with the `party` (`talent`,
`representative` or `licensee`), the signer's `name`, `method: "passkey"` and `signed_at`, and a
`document_url` for the document as a person reads it. Its `status` is `pending_acceptance`,
`current`, `superseded` or `declined`.

Both sides sign every revision; the first is signed with the license itself. When the talent's
side signs a later one, the license's `pending_revision` names it and the licensee receives
[`license.revision_pending`](/api-reference/webhooks/license-revision-pending). The licensee
accepts or declines it at `acceptance_url`, and `current_revision` stays in force until it accepts.
On acceptance, `current_revision` moves to the new revision and you receive
[`license.revised`](/api-reference/webhooks/license-revised). New checks use it at once.
Authorizations already issued keep the revision they name. If the licensee declines, you receive
[`license.revision_declined`](/api-reference/webhooks/license-revision-declined) and the license
keeps its current revision. To narrow a license without waiting, the talent's side suspends or
revokes it.

On the talent's side, a representative can prepare terms as a
[license draft](/api-reference/license-drafts/draft-a-license). A draft grants nothing: a person
signs it with a passkey on a hosted page, and only then does it become a license or a revision.

## Terms and scope

A revision returns the license twice:

* `terms` is the signed document exactly as signed, with its own camelCase field names. This is
  what the talent signed and what `terms_digest` covers.
* `scope` is the same terms in normalized, snake\_case form, generated from `terms` so your code can
  read them. When the two disagree, `terms` wins.

| Signed term | In `scope` | What it says |
| - | - | - |
| `channels`, `territories` | `channels`, `territories` | Where outputs may appear |
| `startsAt`, `endsAt` | `window` | When the license is in force |
| `adjustments` | `adjustments` | Which of the twelve adjustment kinds are allowed, from `reframe` to `endorsement` |
| `prohibitedChanges` | `prohibited_changes` | Adjustments that are never allowed |
| `requiredAttributes` | `required_attributes` | What every output must keep; `identity.samePerson` is always required |
| `neverExtras`, `alwaysExtras`, `creationBrief` | `rules.never`, `rules.always`, `rules.brief` | Rules the talent wrote, as written |
| `releasePolicy` | `release_policy` | `after_checks`, or `per_output_review` when a person releases each output |
| `finalDeliverables` | `final_deliverables` | Outputs that may be delivered in total |
| `knowledgeGrant` | `knowledge`, `monthly_limits.asks` | What a model may draw from a knowledge facet, and how often |
| `voiceLineGrant` | | The voice rules, addressed as `terms.voiceLineGrant` |
| `isExclusive` | `exclusivity` | Exclusivity, recorded only |
| `conflicts` | `conflicts` | Brands the talent is not to be associated with, recorded only |

`scope` also carries `facets`, `modalities`, `kinds`, `purposes`, `commercial`, `platforms`,
`sessions`, `material_access`, `sublicense`, `training`, `disclosure`, `attribution`,
`escalation_window`, `post_revocation` and `compensation`. Reasons and findings point at terms by
address, such as `license:lic_k5zba2q8v1m3n7p4r6s9t0wxyh/terms.channels`.

## The terms digest

`terms_digest` is `sha256:` followed by the lowercase hex SHA-256 of the `terms` object in its
`tlp-json-1` canonical form: UTF-8 JSON, object keys sorted by Unicode code point, no insignificant
whitespace, numbers in shortest round-trip form, and no duplicate keys. Every check, receipt and
authorization carries the digest of the revision it relied on, so you can prove which document a
decision rests on. Recompute it to confirm you hold that document:

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

  function byCodePoint(a: string, b: string): number {
    const x = Array.from(a);
    const y = Array.from(b);
    for (let i = 0; i < Math.min(x.length, y.length); i++) {
      const d = x[i].codePointAt(0)! - y[i].codePointAt(0)!;
      if (d !== 0) return d;
    }
    return x.length - y.length;
  }

  function canonical(value: unknown): string {
    if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
    if (value !== null && typeof value === "object") {
      const obj = value as Record<string, unknown>;
      const keys = Object.keys(obj).sort(byCodePoint);
      return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
    }
    return JSON.stringify(value);
  }

  const revision = await fetch(
    "https://app.reflections.ai/v1/licenses/lic_k5zba2q8v1m3n7p4r6s9t0wxyh/revisions/lrv_p4r6s9t0wxyhk5zba2q8v1m3n7",
    {
      headers: {
        Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
        "Reflections-Version": "2026-10-15",
      },
    },
  ).then((r) => r.json());

  const digest = "sha256:" + createHash("sha256").update(canonical(revision.terms), "utf8").digest("hex");
  console.log(digest === revision.terms_digest);
  ```

  ```python Python theme={null}
  import hashlib, json, os, requests
  from decimal import Decimal

  def js_number(x) -> str:
      """A number as ECMAScript writes it, as tlp-json-1 requires: 1, not 1.0; 1e-7, not 1e-07."""
      if x == 0:
          return "0"
      if x < 0:
          return "-" + js_number(-x)
      _, digits, exponent = Decimal(repr(float(x))).normalize().as_tuple()
      s = "".join(map(str, digits))
      k, n = len(s), len(s) + exponent
      if k <= n <= 21:
          return s + "0" * (n - k)
      if 0 < n <= 21:
          return s[:n] + "." + s[n:]
      if -6 < n <= 0:
          return "0." + "0" * -n + s
      return s[0] + ("." + s[1:] if k > 1 else "") + ("e+" if n > 0 else "e-") + str(abs(n - 1))

  def canonical(value) -> str:
      if isinstance(value, dict):
          return "{" + ",".join(
              json.dumps(k, ensure_ascii=False) + ":" + canonical(value[k]) for k in sorted(value)
          ) + "}"
      if isinstance(value, list):
          return "[" + ",".join(canonical(v) for v in value) + "]"
      if isinstance(value, (int, float)) and not isinstance(value, bool):
          return js_number(value)
      return json.dumps(value, ensure_ascii=False)

  revision = requests.get(
      "https://app.reflections.ai/v1/licenses/lic_k5zba2q8v1m3n7p4r6s9t0wxyh"
      "/revisions/lrv_p4r6s9t0wxyhk5zba2q8v1m3n7",
      headers={
          "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
          "Reflections-Version": "2026-10-15",
      },
  ).json()

  digest = "sha256:" + hashlib.sha256(canonical(revision["terms"]).encode("utf-8")).hexdigest()
  print(digest == revision["terms_digest"])
  ```
</CodeGroup>

Hash `terms`, never `scope`.

## Checked and recorded terms

Every term is either `checked` (Reflections enforces it at the check, the generation or a session
sample) or `recorded` (signed and shown, not enforced). A revision's `enforcement` list states which,
term by term, with the `stage` where each checked term is enforced. Exclusivity and conflicts are
recorded. See [What we enforce](/concepts/what-we-enforce).

## After a revocation

`post_revocation.policy` says what happens to outputs already released if the license is revoked:

| Policy | Released outputs |
| - | - |
| `retain` | Stay usable within their scope and window |
| `cease_distribution` | No new placements after the revocation |
| `withdraw` | Taken down within `withdraw_within`; applies only to a revocation for cause |

See [Revocation](/concepts/revocation).

## Compensation

`compensation` says what the licensee owes the talent: `type` is `none`, `flat`, `per_unit` or
`revenue_share`. A `per_unit` rate names its `units` (`outputs`, `seconds`, `characters`,
`placements` or `impressions`), a `rate` and `per`. A license can set a `minimum` and a `schedule`
(`on_acceptance`, `monthly` or `quarterly`). Amounts are integers in the currency's smallest unit,
with a lowercase ISO 4217 code. Reflections computes statements from it and never moves money. See
[Usage and settlement](/concepts/usage-and-settlement).

## Example: Ridgeline's license K7Q4M2

The signed revision, with `scope` trimmed to the fields above. `terms` is complete, so its
`tlp-json-1` digest is the `terms_digest` shown:

```json theme={null}
{
  "id": "lrv_p4r6s9t0wxyhk5zba2q8v1m3n7",
  "object": "license_revision",
  "license": "lic_k5zba2q8v1m3n7p4r6s9t0wxyh",
  "number": 1,
  "status": "current",
  "source_kind": "issue",
  "terms": {
    "releasePolicy": "perOutputTalentReview",
    "adjustments": {
      "reframe": true, "backgroundExtension": true, "lighting": true, "textOverlay": true,
      "bodyAndAge": false, "expression": false, "hair": false, "pose": false,
      "wardrobe": true, "objects": true, "skinRetouch": false, "endorsement": false
    },
    "channels": ["social", "web"],
    "territories": ["US", "CA"],
    "startsAt": "2026-10-01T00:00:00-04:00",
    "endsAt": "2027-03-31T00:00:00-04:00",
    "windowTimeZone": "America/New_York",
    "requiredAttributes": [
      "identity.samePerson", "appearance.bodyAndAge", "appearance.expression", "appearance.hair",
      "appearance.pose", "appearance.skinRetouch", "context.endorsement"
    ],
    "prohibitedChanges": ["bodyAndAge", "expression", "hair", "pose", "skinRetouch", "endorsement"],
    "neverExtras": ["Do not show alcohol or tobacco."],
    "finalDeliverables": 24,
    "isExclusive": false
  },
  "terms_digest": "sha256:39f18c8bef02b5226b9feaa92e526ed30b81b91650d8a10b503281b169f44431",
  "canonicalization": "tlp-json-1",
  "scope": {
    "modalities": ["image"],
    "purposes": ["advertising"],
    "commercial": true,
    "channels": ["social", "web"],
    "territories": ["US", "CA"],
    "release_policy": "per_output_review",
    "rules": { "never": ["Do not show alcohol or tobacco."], "always": [], "brief": null },
    "final_deliverables": 24,
    "post_revocation": { "policy": "retain" },
    "compensation": {
      "type": "per_unit",
      "per_unit": { "units": "placements", "rate": { "amount": 25000, "currency": "usd" }, "per": 1 },
      "schedule": "monthly"
    }
  },
  "enforcement": [
    { "term": "license:lic_k5zba2q8v1m3n7p4r6s9t0wxyh/terms.channels", "enforcement": "checked", "stage": ["check"] },
    { "term": "license:lic_k5zba2q8v1m3n7p4r6s9t0wxyh/terms.neverExtras", "enforcement": "checked", "stage": ["check", "generation"] },
    { "term": "license:lic_k5zba2q8v1m3n7p4r6s9t0wxyh/terms.isExclusive", "enforcement": "recorded" }
  ],
  "signatures": [
    { "party": "talent", "name": "Maya Okafor", "method": "passkey", "signed_at": "2026-09-27T11:15:00Z" },
    { "party": "licensee", "account": "acct_2q8v1m3n7p4r6s9t0wxyhk5zba", "name": "Ridgeline", "method": "passkey", "signed_at": "2026-09-28T16:02:00Z" }
  ]
}
```

Ridgeline may make images of Maya Okafor for advertising on social media and its website in the
US and Canada until the end of March. Maya Okafor or her manager releases each output, and
Ridgeline reports placements monthly.

No endpoint can sign, accept or widen a license. To ask for a new or wider one, send a
[license request](/api-reference/license-requests/request-a-license); only a signature on the
talent's side makes it a license, and what they sign may differ from what you asked.

## Related

<Columns cols={2}>
  <Card title="Decisions" icon="scale-balanced" href="/concepts/decisions">
    How a check is decided against a revision.
  </Card>

  <Card title="What we enforce" icon="shield-check" href="/concepts/what-we-enforce">
    Checked terms, recorded terms and assurance.
  </Card>

  <Card title="Retrieve a license revision" icon="code" href="/api-reference/licenses/retrieve-a-license-revision">
    The revision object in the reference.
  </Card>

  <Card title="Brands and agencies" icon="briefcase" href="/guides/brands-and-agencies">
    Requesting, accepting and holding licenses.
  </Card>
</Columns>


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