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

# Voice platforms

> Licensed voices held at a provider, scripts and rendered speech, live sessions, and deleting provider copies when consent narrows.

<Note>
  Planned production integration guide. The current sandbox has fictional voice fixtures, but no
  provider voices, material downloads, live sessions, revocation feed or media evaluation.
  Start with the [runnable quickstart](/quickstart) and [sandbox support matrix](/sandbox).
</Note>

A voice platform produces two kinds of output in a talent's voice. Rendered speech, such as a
text-to-speech line, a narration or a dub, is checked like any other output: a check before, a
generation after. Live speech, such as a voice agent or a fan call, runs as a session with a
short-lived token and sampled checks. Both use the talent's voice facet, which often lives at a
model provider.

## Find the voice

A talent's voice is a facet with `type: "voice"`. A talent can have more than one, such as a
speaking voice and a singing voice, so name the one you use with `facet` on the check or session.

When the voice exists at a model provider, the facet lists where in `provider_bindings`. Bindings
are shown only to that provider and to licensees whose license includes `material_access`.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/facets/fct_v1m3n7p4r6s9t0wxyhk5zba2q8 \
    -H "Authorization: Bearer $REFLECTIONS_KEY" \
    -H "Reflections-Version: 2026-10-15"
  ```

  ```typescript TypeScript theme={null}
  const facet = await fetch("https://app.reflections.ai/v1/facets/fct_v1m3n7p4r6s9t0wxyhk5zba2q8", {
    headers: { Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`, "Reflections-Version": "2026-10-15" },
  }).then((r) => r.json());
  ```

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

  facet = requests.get(
      "https://app.reflections.ai/v1/facets/fct_v1m3n7p4r6s9t0wxyhk5zba2q8",
      headers={"Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}", "Reflections-Version": "2026-10-15"},
  ).json()
  ```
</CodeGroup>

```json theme={null}
{
  "id": "fct_v1m3n7p4r6s9t0wxyhk5zba2q8",
  "object": "facet",
  "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
  "type": "voice",
  "label": "Speaking voice",
  "status": "ready",
  "modalities": ["speech"],
  "provider_bindings": [
    { "provider": "example-voice-provider", "kind": "voice", "external_id": "vx_4f9a2c", "status": "active" }
  ]
}
```

Call the provider with the binding's `external_id`. Subscribe to `facet.updated`: when a binding's
`status` becomes `deleted` or the facet's `status` becomes `withdrawn`, stop using that voice.

If your license includes `material_access` for the voice, you can download its samples with
[`GET /v1/facets/{facet_id}/material`](/api-reference/facets/retrieve-licensed-material) and create
the voice at your own provider. That copy is yours to delete: the material comes with a
`delete_material` obligation, and every download is recorded on the talent's record.

## Rendered speech

Send the words as an input with `role: "script"`, and `use.modality: "speech"`. Speech in the
talent's voice is `kind: "impersonation"`.

* **Interactive products** pass `on_escalate: "deny"`. A request that would wait for a reviewer
  returns `deny` with `review_required` at once.
* **Many lines for one user** ask for `reuse`, up to 1,000 generations and `PT15M`. Each line is
  still submitted as its own generation and checked on its own audio and words.
* **Talent rules** arrive as `generation_instructions`. Apply them to the words before they are
  spoken; if a model writes the script, put them in its system prompt.

Submit every rendered output. Speech up to 60 seconds returns its decision in the response; longer
audio returns `status: "checking"` and the decision arrives as `generation.allowed` or
`generation.denied`. Send the words in `transcript` when you have them; Reflections transcribes
the audio either way. Rules on speech fail with reasons such as `prohibited_speech`,
`narration_mismatch` and `voice_rule`. The [voice integration design](/quickstarts/voice) describes the target path.

### Voice samples from users

If your users can upload a voice to clone, send the sample as an input with
`role: "voice_sample"`. The check matches it against talent who opted into protection, and a
match appears in `talent` with `matched_by: "voice_sample"`. No match means the sample matched no
protected talent. It does not license the voice.

## Live sessions

A session runs one check at the start, then keeps a token that expires 5 minutes after issue.

| You | When |
| - | - |
| Renew with `POST /v1/sessions/{session_id}/renew` | Every 60 seconds |
| Submit an audio sample | At least once per 15 seconds of generated speech |
| Submit a transcript sample | Every turn, when `sampling.transcript` is `every_turn` |
| Stop output | Within 1 second of a `deny` sample, `409 session_not_active` on renewal, `session.terminated`, or the current token expiring with no renewal |

Pass the current session token to your realtime voice provider so it can verify the session
offline. Stay connected to `stream_url`: it pushes `session.terminated` the moment it happens,
and the p95 target from a revocation to that message is 2 seconds.

Sessions never escalate, because nobody can review a live call. Anything that would escalate is
`deny`. A license that holds every output for release cannot start a session; the session returns
`status: "denied"` with `session_not_permitted`.

Two bounds follow from the design. Up to 15 seconds of speech between samples is unchecked, and a
revoked license leaves the current token valid until it expires. Stopping on the stream message
keeps the second bound to seconds. See [Real-time sessions](/build/real-time-sessions).

## Platform licenses for your users

A fan voice product usually runs under a platform license held by your platform, not a brand's.
Its scope says whether your end users may make personal, non-commercial speech
(`sublicense: "end_users_personal"`) and sets `monthly_limits` in `seconds` or `characters`.
Send a stable `end_user.id` on every check and session so use is counted per end user. A check
past a monthly limit denies with `monthly_limit_reached`.

## Delete provider copies on revocation

When a talent revokes a license or withdraws a voice, `revocation.created` lists what you must do
in `required_actions`. For a voice held at a provider, that includes `delete_provider_copies`, and
`provider_copies` names each copy by `provider` and `external_id`. Delete each one, then
acknowledge before `acknowledge_by`, 24 hours after `effective_at`. Derive the
`Idempotency-Key` from the revocation ID, so a retry after a crash cannot acknowledge twice.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/revocations/rvk_s9t0wxyhk5zba2q8v1m3n7p4r6/acknowledgements \
    -H "Authorization: Bearer $REFLECTIONS_KEY" \
    -H "Reflections-Version: 2026-10-15" \
    -H "Idempotency-Key: ack-rvk_s9t0wxyhk5zba2q8v1m3n7p4r6" \
    -H "Content-Type: application/json" \
    -d '{
      "actions": ["stop_generation", "purge_cached_decisions", "delete_provider_copies"],
      "deleted_copies": [
        { "provider": "example-voice-provider", "external_id": "vx_4f9a2c", "deleted_at": "2026-10-17T19:12:00Z" }
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  // revocation is the revocation.created event's data.object. deletedAt comes from your own record
  // of the deletion, so a retry sends the same body.
  const res = await fetch(`https://app.reflections.ai/v1/revocations/${revocation.id}/acknowledgements`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
      "Reflections-Version": "2026-10-15",
      "Idempotency-Key": `ack-${revocation.id}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      actions: ["stop_generation", "purge_cached_decisions", "delete_provider_copies"],
      deleted_copies: [{ provider: "example-voice-provider", external_id: "vx_4f9a2c", deleted_at: deletedAt }],
    }),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); // retry with the same key
  ```

  ```python Python theme={null}
  # revocation is the revocation.created event's data.object. deleted_at comes from your own record
  # of the deletion, so a retry sends the same body.
  res = requests.post(
      f"https://app.reflections.ai/v1/revocations/{revocation['id']}/acknowledgements",
      headers={
          "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
          "Reflections-Version": "2026-10-15",
          "Idempotency-Key": f"ack-{revocation['id']}",
      },
      json={
          "actions": ["stop_generation", "purge_cached_decisions", "delete_provider_copies"],
          "deleted_copies": [{"provider": "example-voice-provider", "external_id": "vx_4f9a2c", "deleted_at": deleted_at}],
      },
  )
  res.raise_for_status()  # retry with the same key
  ```
</CodeGroup>

Include every action in `required_actions`. The acknowledgement is shown on the talent's record;
an unacknowledged revocation becomes `revocation.overdue`. See
[Handling revocations](/build/handling-revocations).

## Report usage

Voice licenses usually price `seconds` or `characters`. Report each generation and each session
with a usage record that names it, within the `usage_report` obligation's `within`. See
[Reporting usage](/build/reporting-usage).

## What can go wrong

| Response | Cause | What to do |
| - | - | - |
| Session `status: "denied"` with `session_not_permitted` | The license holds every output for release | Render speech as generations instead |
| `409 session_not_active` on renew | The license was revoked, suspended or ended, or the session was terminated | Stop output at once and end the session |
| Sample `deny` | A sample broke a rule | Stop output within 1 second and end the session |
| `409 authorization_consumed` | A reused authorization is used up | Run a new check |
| `deny` with `facet_not_licensed` | The license does not cover this voice | Name a licensed facet, or request a change |

## Related

<Columns cols={2}>
  <Card title="Voice integration design" icon="microphone" href="/quickstarts/voice">
    Proposed speech-line and live-session workflows.
  </Card>

  <Card title="Session stream" icon="plug" href="/api-reference/realtime/session-stream">
    Every message on the WebSocket.
  </Card>

  <Card title="Talent and facets" icon="user" href="/concepts/talent-and-facets">
    What a facet is and which identity each one covers.
  </Card>

  <Card title="Model providers" icon="server" href="/guides/model-providers">
    How your provider verifies authorization and session tokens.
  </Card>
</Columns>


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