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

# Talent representatives

> Run a talent's review queue, license drafts, revocations and statements from your own systems, under the mandate the talent gave you.

<Note>
  Planned Talent API integration guide. Mandates, review queues, license changes, revocations
  and statements are not served through the current sandbox. The examples below are proposed API
  workflows, not instructions for the current Talent Portal.
  Start with the [runnable quickstart](/quickstart) and [sandbox support matrix](/sandbox).
</Note>

A talent's own live key can already read the licenses she issued
(`GET /v1/licenses?role=issuer`) and the state of each output made under one
(`GET /v1/generations?license=…`; add `release_status=awaiting_release` for the finals waiting
for her release). Those reads are read-only and show no licensee prompts, references, bytes or
metadata. Releasing or declining through the API, and the mandate workflows below, remain planned.

A representative is an account that acts for a talent under a **mandate**: an agency, a manager,
an estate, a union or legal counsel. This guide describes the proposed API workflow under that
mandate. Availability in the Talent Portal is separate and is not established by these examples.

```mermaid theme={null}
flowchart LR
    T[Talent] -->|appoints, with powers| M[Mandate]
    M --> R[Representative]
    R -->|resolves| E[Escalations and held outputs]
    R -->|drafts, a person signs| L[Licenses]
    R -->|suspends and revokes| L
    R -->|confirms and disputes| S[Statements]
```

## The mandate

The talent appoints you in the Talent Portal and chooses your powers. In live mode your account
needs the `represent_talent` capability, which Reflections adds when it approves the account. No
endpoint creates a mandate in this proposed contract. The current sandbox does not provide
mandates or the representative workflows below.

| Power | Lets you |
| - | - |
| `issue_licenses` | Answer license requests and draft licenses for a person to sign |
| `review_outputs` | Resolve escalated checks, and release or decline held outputs |
| `revoke` | Suspend, resume and revoke the talent's licenses |
| `receive_statements` | Read statements, confirm payments and dispute statements |

Your mandate is public. Every platform that reads the talent's record sees your account, name,
role and powers in `representation`, so a licensee knows who may sign. The talent can end the
mandate at any time; that is recorded as a revocation with `scope: "mandate"`, and you receive
[`mandate.ended`](/api-reference/webhooks/mandate-ended).

## Set up

<Steps>
  <Step title="Create a restricted key">
    When these operations become available, restrict a key to the scopes
    your systems use: `mandates:read`, `licenses:read`, `escalations:write`,
    `license_requests:write`, `license_drafts:write`, `licenses:write`, `revocations:write`,
    `statements:read`, `statements:write`, and for audits `usage:read` and `generations:read`.
    Webhook setup needs `webhooks:manage`. Give a review tool only `escalations:write`. See
    [Authentication](/api-reference/authentication#restricted-keys).
  </Step>

  <Step title="Subscribe to the talent-side events">
    Create a [webhook endpoint](/api-reference/webhooks/overview) for
    `escalation.created`, `license_request.created`, `license_draft.signed`,
    `license_draft.declined`, `mandate.created` and `mandate.ended`. Add the events of the
    licenses you manage: `license.accepted`, `license.declined`, `license.revised`,
    `license.revision_declined`, `license.expired`, `revocation.created`, `revocation.overdue`,
    `statement.issued` and `statement.disputed`. An endpoint lists exact types, or `["*"]` for
    every type.
  </Step>

  <Step title="Read your mandates">
    Talent-side calls take no `Reflections-Account` header. The talent named in the call, and
    your mandate for them, decide what you may do. A call your mandate does not cover fails with
    `403 mandate_required`.

    ```bash theme={null}
    curl "https://app.reflections.ai/v1/mandates?status=active" \
      -H "Authorization: Bearer rk_live_…" \
      -H "Reflections-Version: 2026-10-15"
    ```

    ```json theme={null}
    {
      "object": "list",
      "data": [
        {
          "id": "mnd_7nb1ef0wykcj3mkwaba3x20ez6",
          "object": "mandate",
          "livemode": true,
          "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
          "talent_name": "Maya Okafor",
          "representative": "acct_6m8n0p2q4r6s8t0v2w4x6y8z0b",
          "role": "manager",
          "powers": ["issue_licenses", "review_outputs", "revoke", "receive_statements"],
          "status": "active",
          "starts_at": "2026-06-12T10:00:00Z",
          "ends_at": null,
          "ended_at": null,
          "revocation": null,
          "created_at": "2026-06-12T10:00:00Z"
        }
      ],
      "has_more": false,
      "next_cursor": null
    }
    ```
  </Step>
</Steps>

## Review escalations

With `review_outputs`, you decide checks that Reflections escalated to the talent's side under
the talent's customer licenses. [`escalation.created`](/api-reference/webhooks/escalation-created)
tells you when one arrives, and [`GET /v1/escalations`](/api-reference/escalations/list-escalations)
is the queue, oldest deadline first.

| `reason` | Why it reached you |
| - | - |
| `judgment` | A rule the talent wrote needs a person to read it |
| `unverifiable` | A protected attribute could not be confirmed from the request |
| `evaluation_unavailable` | The automated check could not finish |
| `release_review` | The license has a person release every output. See [Release held outputs](#release-held-outputs). |

Each escalation shows the request as the platform sent it, the use, the licensee, the platform,
and the term the question rests on:

```json theme={null}
{
  "id": "esc_t0wxyhk5zba2q8v1m3n7p4r6s9",
  "object": "escalation",
  "livemode": true,
  "reason": "judgment",
  "reviewer": "talent",
  "status": "open",
  "expires_at": "2026-10-24T18:04:00Z",
  "subject": { "object": "check", "id": "chk_r6s9t0wxyhk5zba2q8v1m3n7p4" },
  "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
  "license": "lic_k5zba2q8v1m3n7p4r6s9t0wxyh",
  "license_revision": "lrv_p4r6s9t0wxyhk5zba2q8v1m3n7",
  "licensee": "acct_2q8v1m3n7p4r6s9t0wxyhk5zba",
  "licensee_name": "Ridgeline",
  "platform": "acct_7p4r6s9t0wxyhk5zba2q8v1m3n",
  "platform_name": "Northlight",
  "use": { "modality": "image", "kind": "depiction", "purpose": "advertising", "commercial": true, "channels": ["social"], "territories": ["US"] },
  "inputs": [
    { "role": "prompt", "text": "Maya Okafor at a trailhead café with two friends, paper cups on the table, morning light" }
  ],
  "reasons": [
    { "code": "prohibited_custom_term", "stage": "check", "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s", "term": "license:lic_k5zba2q8v1m3n7p4r6s9t0wxyh/terms.neverExtras", "sentence": "Do not show alcohol or tobacco." }
  ],
  "request_digest": "sha256:c3e5a7b9d1f3c5e7a9b1d3f5c7e9a1b3d5f7c9e1a3b5d7f9c1e3a5b7d9f1c3e5",
  "review_url": "https://app.reflections.ai/review/esc_t0wxyhk5zba2q8v1m3n7p4r6s9",
  "decision": null,
  "decided_by": null,
  "created_at": "2026-10-17T18:04:00Z"
}
```

Show it to a person in your own tool, then record their decision, or send them to `review_url`,
where they decide in the Talent Portal. `decided_by` names them; the talent's audit record shows
the decision with your account and that name. A `note` of up to 500 characters
reaches the platform as `escalation.note`.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/escalations/esc_t0wxyhk5zba2q8v1m3n7p4r6s9/resolve \
    -H "Authorization: Bearer rk_live_…" \
    -H "Reflections-Version: 2026-10-15" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "decision": "allow",
      "note": "Fine as long as the cups hold coffee.",
      "decided_by": { "name": "Lena Brooks" }
    }'
  ```

  ```typescript TypeScript theme={null}
  const escalation = await fetch(
    "https://app.reflections.ai/v1/escalations/esc_t0wxyhk5zba2q8v1m3n7p4r6s9/resolve",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
        "Reflections-Version": "2026-10-15",
        "Idempotency-Key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        decision: "allow",
        note: "Fine as long as the cups hold coffee.",
        decided_by: { name: "Lena Brooks" },
      }),
    },
  ).then((r) => r.json());
  ```

  ```python Python theme={null}
  import os, uuid, requests

  escalation = requests.post(
      "https://app.reflections.ai/v1/escalations/esc_t0wxyhk5zba2q8v1m3n7p4r6s9/resolve",
      headers={
          "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
          "Reflections-Version": "2026-10-15",
          "Idempotency-Key": str(uuid.uuid4()),
      },
      json={
          "decision": "allow",
          "note": "Fine as long as the cups hold coffee.",
          "decided_by": {"name": "Lena Brooks"},
      },
  ).json()
  ```
</CodeGroup>

The platform receives [`check.resolved`](/api-reference/webhooks/check-resolved) and may generate.
An allow covers exactly the request that was escalated, bound by its `request_digest`; anything
different needs a new check. Decide before `expires_at`: 7 days, or the license's shorter
`escalation_window`. An escalated check left unanswered becomes `deny` with `hold_expired`, and
resolving a decided or expired escalation returns `409 escalation_closed`.

When a check invokes several talent, each talent's side decides their own escalation, and
`talent[].escalation` names each one. The check is allowed only when every one allows; the first
`deny` decides it.

<Warning>
  Resolve escalations only on a person's decision. If the answer to a kind of request is always
  the same, change the license instead, so the answer becomes a checked term that every check
  applies.
</Warning>

Escalations under platform licenses, such as a voice app's license for its users' personal use,
are reviewed by Reflections trust and safety, with a response target of one business day. They
do not appear in your queue.

## Release held outputs

When a license's `release_policy` is `per_output_review`, every output waits for a person before
it can be delivered. Each held output is an escalation with `reason: "release_review"` and
`subject.object: "generation"`. It carries the clean bytes at `artifact.url`, open for 15
minutes, and the `findings` Reflections recorded on them: which protected attributes held, which
of the talent's rules held, and any rule marked `unchecked` because it could not be read.

Resolve it the same way. `allow` releases the output: the platform receives
[`generation.released`](/api-reference/webhooks/generation-released) and may deliver it with its
receipt. `deny` declines it: the platform receives
[`generation.denied`](/api-reference/webhooks/generation-denied) with `review_declined` and must
never deliver it. A held output nobody releases by `expires_at` (7 days, or the license's shorter
`escalation_window`) expires with `review_expired`. Until release, the platform may show its user
only a preview carrying the draft watermark.

## Answer license requests

With `issue_licenses`, you answer the requests licensees send the talent.
[`license_request.created`](/api-reference/webhooks/license-request-created) tells you when one
arrives, and [`GET /v1/license_requests?talent=…&status=sent`](/api-reference/license-requests/list-license-requests)
lists the ones waiting. A request shows the use that prompted it when it came from a denied
check, the scope asked for, the offer and the licensee's note.

To agree, draft a license that names the request. You may change any term: channels,
territories, dates, the appearance changes allowed, the talent's rules (up to 160 characters
each), the release policy, the escalation window, which platforms may run checks, compensation,
and what happens to released outputs after a revocation (`post_revocation`).

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/license_drafts \
    -H "Authorization: Bearer rk_live_…" \
    -H "Reflections-Version: 2026-10-15" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
      "license_request": "lrq_ykaa695mrd8c92h3ahg476m47n",
      "scope": {
        "channels": ["social", "web"],
        "territories": ["US"],
        "rules": { "never": ["Do not show alcohol or tobacco."], "always": [], "brief": null },
        "release_policy": "per_output_review"
      },
      "signer": "talent",
      "note": "Social and web in the US only, with every output reviewed."
    }'
  ```

  ```typescript TypeScript theme={null}
  const draft = await fetch("https://app.reflections.ai/v1/license_drafts", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
      "Reflections-Version": "2026-10-15",
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      talent: "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
      license_request: "lrq_ykaa695mrd8c92h3ahg476m47n",
      scope: {
        channels: ["social", "web"],
        territories: ["US"],
        rules: { never: ["Do not show alcohol or tobacco."], always: [], brief: null },
        release_policy: "per_output_review",
      },
      signer: "talent",
      note: "Social and web in the US only, with every output reviewed.",
    }),
  }).then((r) => r.json());
  // Send draft.signing_url to the signer.
  ```

  ```python Python theme={null}
  import os, uuid, requests

  draft = requests.post(
      "https://app.reflections.ai/v1/license_drafts",
      headers={
          "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
          "Reflections-Version": "2026-10-15",
          "Idempotency-Key": str(uuid.uuid4()),
      },
      json={
          "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
          "license_request": "lrq_ykaa695mrd8c92h3ahg476m47n",
          "scope": {
              "channels": ["social", "web"],
              "territories": ["US"],
              "rules": {"never": ["Do not show alcohol or tobacco."], "always": [], "brief": None},
              "release_policy": "per_output_review",
          },
          "signer": "talent",
          "note": "Social and web in the US only, with every output reviewed.",
      },
  ).json()
  # Send draft["signing_url"] to the signer.
  ```
</CodeGroup>

When you name a request, `scope` holds only what you change from what the licensee asked for.
The draft returns the full terms the signer will see, the `terms_digest` its revision will carry
if it is signed as drafted, and `signing_url`. Change an open draft with
[`POST /v1/license_drafts/{draft_id}`](/api-reference/license-drafts/update-a-license-draft), or
[discard it](/api-reference/license-drafts/discard-a-license-draft).

To refuse, [decline the request](/api-reference/license-requests/decline-a-license-request) with a
`decision_note`. The licensee receives it on
[`license_request.declined`](/api-reference/webhooks/license-request-declined).

## Sign

A draft grants nothing. A person signs it with a passkey at `signing_url`, in the Talent Portal:
the talent when `signer` is `talent`, or a person at your account with `issue_licenses` when it is
`representative`. No endpoint signs a license.

The signing page shows every term, marked `checked`, which Reflections enforces, or `recorded`,
which is signed and shown but not enforced, such as `exclusivity` and `conflicts`. Rely on checked
terms for anything that must be enforced. See [What we enforce](/concepts/what-we-enforce). The
signer may change terms before signing.

When it is signed, you receive [`license_draft.signed`](/api-reference/webhooks/license-draft-signed)
with `license`, `license_revision` and `signed_as_drafted`. A license a representative signs
records `issued_by.party: "representative"` with your account and mandate, and the revision's
`signatures` lists the signer. If the signer declines, you receive
[`license_draft.declined`](/api-reference/webhooks/license-draft-declined).

The licensee then accepts on its hosted acceptance page. A new license is `pending_acceptance`
until it does, and you receive `license.accepted`. A new revision waits in the license's
`pending_revision`, and the current revision stays in force until the licensee accepts it. You
receive [`license.revised`](/api-reference/webhooks/license-revised) when it does, or
[`license.revision_declined`](/api-reference/webhooks/license-revision-declined). To narrow a
license at once instead, suspend or revoke it.

Amendments and renewals are drafts too. Set `kind` to `amend` or `renew`, name the `license`, and
send only the terms that change, such as a new `window` for a renewal.

## Suspend, resume and revoke

With `revoke`, you can narrow consent at once.

* **Suspend** a license during a dispute with
  [`POST /v1/licenses/{license_id}/suspend`](/api-reference/licenses/suspend-a-license). Checks deny
  with `license_suspended`, live sessions end, held outputs stay held, and you can
  [resume it](/api-reference/licenses/resume-a-license) later.
* **Revoke** a license with
  [`POST /v1/revocations`](/api-reference/revocations/create-a-revocation). New checks deny,
  pending and held work under the license is void, and live sessions end within seconds. Each
  platform that used it must stop generating, purge cached decisions, delete copies held at model
  providers and acknowledge by `acknowledge_by`, 24 hours later.
* **Revoke for cause** after a breach, with `for_cause: true` and the `cause` platforms will read.
  Only then can a license's `withdraw` policy require released outputs to be taken down. Platforms
  acknowledge the takedown separately, by `post_revocation.withdraw_by`.

Only the talent withdraws a facet, withdraws consent everywhere, or ends your mandate, in the
Talent Portal. Those revocations reach you as
[`revocation.created`](/api-reference/webhooks/revocation-created).

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/revocations \
    -H "Authorization: Bearer rk_live_…" \
    -H "Reflections-Version: 2026-10-15" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "license": "lic_31x87g772xdcz1n1za29601rbt",
      "note": "The campaign ended early."
    }'
  ```

  ```typescript TypeScript theme={null}
  const revocation = await fetch("https://app.reflections.ai/v1/revocations", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
      "Reflections-Version": "2026-10-15",
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      license: "lic_31x87g772xdcz1n1za29601rbt",
      note: "The campaign ended early.",
    }),
  }).then((r) => r.json());
  ```

  ```python Python theme={null}
  revocation = requests.post(
      "https://app.reflections.ai/v1/revocations",
      headers={
          "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
          "Reflections-Version": "2026-10-15",
          "Idempotency-Key": str(uuid.uuid4()),
      },
      json={
          "license": "lic_31x87g772xdcz1n1za29601rbt",
          "note": "The campaign ended early.",
      },
  ).json()
  ```
</CodeGroup>

A revocation is in effect when the call returns, and it cannot be undone. Send an
`Idempotency-Key` and retry with the same key if you do not know whether it went through. The
response lists `required_actions`, `acknowledge_by` and what it cut in `affected`. Read each
platform's acknowledgement, and any that is overdue, with
[`GET /v1/revocations?talent=…`](/api-reference/revocations/list-revocations).

Released outputs follow the license's `post_revocation` term: `retain`, `cease_distribution` or,
for cause, `withdraw`. See [Revocation](/concepts/revocation).

## Statements and audits

With `receive_statements`, you read each license's monthly statement with
[`GET /v1/statements?talent=…`](/api-reference/statements/list-statements): units reported by
platforms, units Reflections observed from checks and generations, the greater of the two billed,
and whether they reconciled. Reflections does not move money.

* When the licensee records a payment, confirm it arrived with
  [`POST /v1/statements/{statement_id}/payments/{payment_id}/confirm`](/api-reference/statements/confirm-a-payment).
  The statement becomes `paid` when confirmed payments reach `amount_due`.
* If a statement is wrong, [dispute it](/api-reference/statements/dispute-a-statement) with the
  reason the licensee will read. It is reissued when the licensee's usage records or payments
  change.

For an audit, [`GET /v1/usage_records?talent=…`](/api-reference/usage-records/list-usage-records)
and [`GET /v1/generations?talent=…`](/api-reference/generations/list-generations) return every
usage record and every generation under the talent's licenses: the platform, the licensee, the
decision, the findings and the receipt. On the talent's audit request, Reflections can ask a
platform for its generation logs for a stated period. Receipts, usage records and statements are
kept for 7 years. Prompt text is kept only on `deny` and `escalate`, for 30 days.

## Verify what you find

Receipts and verifications work with any key, or none at the public rate limit, so you can check
a file you found that claims to be licensed:

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/verifications \
    -H "Reflections-Version: 2026-10-15" \
    -F file=@found-online.jpg
  ```

  ```typescript TypeScript theme={null}
  import { readFile } from "node:fs/promises";

  const form = new FormData();
  form.append("file", new Blob([await readFile("found-online.jpg")], { type: "image/jpeg" }), "found-online.jpg");

  const verification = await fetch("https://app.reflections.ai/v1/verifications", {
    method: "POST",
    headers: { "Reflections-Version": "2026-10-15" },
    body: form,
  }).then((r) => r.json());
  ```

  ```python Python theme={null}
  import requests

  with open("found-online.jpg", "rb") as f:
      verification = requests.post(
          "https://app.reflections.ai/v1/verifications",
          headers={"Reflections-Version": "2026-10-15"},
          files={"file": ("found-online.jpg", f, "image/jpeg")},
      ).json()
  ```
</CodeGroup>

| `result` | Meaning |
| - | - |
| `verified` | A valid receipt matches. Read `receipt.licensee`, `receipt.platform` and `receipt.use_status` |
| `invalid` | The file carries a receipt or manifest that fails verification |
| `not_found` | No receipt matches. This says nothing either way about whether the use is licensed |

Send an `invalid` result, or a file that uses the talent with no receipt, to
[Reflections support](/production/support) with the verification ID.

## Current test coverage

The sandbox provides fictional licenses, checks, generations and an escalation-resolution helper.
It does not provide mandates, a signing page, representative review queues, license acceptance
helpers or a dashboard reset action. See the [sandbox support matrix](/sandbox) for supported
tests and the [quickstart](/quickstart) for an executable path.

## Related

<Columns cols={2}>
  <Card title="Parties" icon="users" href="/concepts/parties">
    Talent, representatives, licensees, platforms and end users.
  </Card>

  <Card title="Escalation" icon="hourglass-half" href="/concepts/escalation">
    Who reviews, how long it can take, and what expiry means.
  </Card>

  <Card title="Licenses" icon="file-signature" href="/concepts/licenses">
    Revisions, digests and the terms a license can carry.
  </Card>

  <Card title="Revocation" icon="ban" href="/concepts/revocation">
    What a revocation cuts, and what platforms must do.
  </Card>
</Columns>


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