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

# Resolve talent

> Name talent explicitly, build autocomplete and disambiguation, and match prompts locally with the name index.

<Note>
  Protocol reference. The current sandbox matches fictional fixtures by explicit ID or text.
  Standalone resolution, the name index and image or voice recognition described below are not
  available.
  Start with the [runnable quickstart](/quickstart) and [sandbox support matrix](/sandbox).
</Note>

Every check has to know which talent a request invokes. You can name them yourself, let the check
find them in the inputs, or match prompts on your side first and send only the ones that hit. This
page covers all three, and the mistakes that let an unlicensed use through.

## Choose how to name talent

| Your product | What you send | `matched_by` on the check |
| - | - | - |
| A cast picker, catalog or voice list where the user selects a person | `talent: ["tal_…"]` | `explicit` |
| Free-text prompts | Nothing extra. Omit `talent` and the check resolves the inputs. | `name`, `alias`, `indirect` |
| Uploaded photos or voice recordings | The media as `reference` or `voice_sample` inputs | `reference_image`, `voice_sample` |
| A description with no name, such as "a tall trail runner with braids" | Nothing | Not resolved |

A description is not matched, and outputs are checked only for the talent their check covered. See
[Generation checks](/concepts/generation-checks#which-talent-are-checked).

## Name talent explicitly

When your user picks a person from a list, send their ID. The check reports them with
`matched_by: "explicit"` and `confidence: "exact"`, and still resolves the inputs for anyone else.

To build the list, search talent by name and keep only those the licensee holds a license for:

<CodeGroup>
  ```bash cURL theme={null}
  curl -G https://app.reflections.ai/v1/talent \
    -H "Authorization: Bearer rk_test_…" \
    -H "Reflections-Version: 2026-10-15" \
    --data-urlencode "q=Maya" \
    --data-urlencode "licensed=true" \
    --data-urlencode "facet_type=voice"
  ```

  ```typescript TypeScript theme={null}
  const url = new URL("https://app.reflections.ai/v1/talent");
  url.searchParams.set("q", "Maya");
  url.searchParams.set("licensed", "true");
  url.searchParams.set("facet_type", "voice");

  const talent = await fetch(url, {
    headers: {
      Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
      "Reflections-Version": "2026-10-15",
    },
  }).then((r) => r.json());
  ```

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

  headers = {
      "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
      "Reflections-Version": "2026-10-15",
  }

  talent = requests.get(
      "https://app.reflections.ai/v1/talent",
      headers=headers,
      params={"q": "Maya", "licensed": "true", "facet_type": "voice"},
  ).json()
  ```
</CodeGroup>

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "id": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
      "object": "talent",
      "name": "Maya Okafor",
      "facets": [
        { "id": "fct_5zba2q8v1m3n7p4r6s9t0wxyhk", "type": "likeness", "label": "Studio likeness", "status": "ready" },
        { "id": "fct_v1m3n7p4r6s9t0wxyhk5zba2q8", "type": "voice", "label": "Speaking voice", "status": "ready" }
      ]
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

When a platform acts for a licensee such as Ridgeline, add `Reflections-Account:
acct_2q8v1m3n7p4r6s9t0wxyhk5zba` so `licensed=true` filters by Ridgeline's licenses. Pass the
chosen ID in the check's `talent`, and the facet in `facet` when the talent has more than one of a
type.

An explicit ID covers who your user picked. It does not cover anyone else the prompt names. If the
prompt also says "with Iris Calder", the check still finds her and decides for both. See
[Multi-talent requests](/build/multi-talent).

## Let the check resolve the inputs

Omit `talent` and send the request as your model receives it. The check matches names, aliases,
indirect references and reference media, and lists each talent it found in `talent[]`. Every
candidate at `medium` confidence or higher is treated as invoked. Read `matched_by` and
`confidence` to see why a talent is there.

This is the default for free-text products. You do not need to call anything before the check.

## Ask "did you mean"

Call [`POST /v1/resolutions`](/api-reference/resolutions/resolve-talent) only when your own
interface needs candidates, such as autocomplete or a confirmation prompt. A resolution never
decides anything. A restricted key needs the `talent:read` scope to create one, not `checks:write`;
the same scope covers listing talent and downloading the name index. See
[Restricted keys](/api-reference/authentication#restricted-keys).

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/resolutions \
    -H "Authorization: Bearer rk_test_…" \
    -H "Reflections-Version: 2026-10-15" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "inputs": [{ "role": "prompt", "text": "Okafor running a ridge trail at dawn" }],
      "modality": "image"
    }'
  ```

  ```typescript TypeScript theme={null}
  const resolution = await fetch("https://app.reflections.ai/v1/resolutions", {
    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({
      inputs: [{ role: "prompt", text: "Okafor running a ridge trail at dawn" }],
      modality: "image",
    }),
  }).then((r) => r.json());
  ```

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

  resolution = requests.post(
      "https://app.reflections.ai/v1/resolutions",
      headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
      json={
          "inputs": [{"role": "prompt", "text": "Okafor running a ridge trail at dawn"}],
          "modality": "image",
      },
  ).json()
  ```
</CodeGroup>

A surname alone is a weak match, so the resolution is `ambiguous`:

```json theme={null}
{
  "id": "res_2t4w6y8a0c2e4g6j8m0p2r4t6w",
  "object": "resolution",
  "livemode": false,
  "status": "ambiguous",
  "candidates": [
    {
      "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
      "name": "Maya Okafor",
      "matched_by": "name",
      "confidence": "medium",
      "span": { "input": 0, "start": 0, "end": 6, "text": "Okafor" },
      "licensed": true,
      "declaration": "license_required"
    }
  ],
  "use_kind": "depiction",
  "created_at": "2026-10-17T18:00:00Z"
}
```

Use `span` to highlight the matched words. Then send the check with the user's answer:

| The user says | Send on the check |
| - | - |
| Yes, Maya Okafor | `"talent": ["tal_9t0wxyhk5zba2q8v1m3n7p4r6s"]` |
| No, someone else | `"exclude_talent": ["tal_9t0wxyhk5zba2q8v1m3n7p4r6s"]` |

`use_kind` classifies how the inputs use the person. `reference` (talking about a person, with no
synthesis) needs no license; the [LLM assistants guide](/guides/llm-assistants) shows how to route
it. It is a classification for routing, not a legal finding.

## Match locally with the name index

If you do not want to send every prompt to Reflections, download the signed name index and match on
your side. A local hit means "send a check". No hit means only that no name matched.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/talent/index \
    -H "Authorization: Bearer rk_test_…" \
    -H "Reflections-Version: 2026-10-15" \
    -H 'If-None-Match: "20261017.17"' \
    -D headers.txt -o index.json
  ```

  ```typescript TypeScript theme={null}
  let cached: { etag: string | null; index?: unknown } = { etag: null };

  const res = await fetch("https://app.reflections.ai/v1/talent/index", {
    headers: {
      Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
      "Reflections-Version": "2026-10-15",
      ...(cached.etag ? { "If-None-Match": cached.etag } : {}),
    },
  });
  if (res.status === 200) {
    cached = { etag: res.headers.get("ETag"), index: await res.json() };
  }
  // 304: the index you hold is current.
  ```

  ```python Python theme={null}
  cached = {"etag": None}

  extra = {"If-None-Match": cached["etag"]} if cached["etag"] else {}
  res = requests.get("https://app.reflections.ai/v1/talent/index", headers={**headers, **extra})
  if res.status_code == 200:
      cached = {"etag": res.headers["ETag"], "index": res.json()}
  # 304: the index you hold is current.
  ```
</CodeGroup>

```json theme={null}
{
  "object": "talent_index",
  "version": "20261017.17",
  "generated_at": "2026-10-17T17:00:00Z",
  "expires_at": "2026-10-17T19:00:00Z",
  "normalization": "nfkc_casefold",
  "entries": [
    { "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s", "names": ["maya okafor"], "common": false }
  ],
  "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InJmbC0yMDI2LTEwLWRvY3MzIn0..nvnbFix3EszwJJmpVo1MOtOlUCdv_L4WIz42je2-OFKx71H_rrs-2Qaoe_UzwxmgABOcZDuobG53nXoUsuK_vg"
}
```

<Steps>
  <Step title="Verify the signature">
    `signature` is a detached JWS over the `tlp-json-1` canonical form of `entries`. Verify it
    against the keys at `https://app.reflections.ai/.well-known/jwks.json` before you use the
    index.
  </Step>

  <Step title="Normalize the prompt the same way">
    Apply Unicode NFKC normalization and case folding (`nfkc_casefold`) to every text input, then
    look for each entry's `names` on word boundaries.
  </Step>

  <Step title="Send a check on any hit">
    A hit on an entry with `common: true` is weak evidence, but it still needs a check. You can omit
    `talent` and let the check resolve the full request.
  </Step>

  <Step title="Keep it current">
    Refresh at least hourly with `If-None-Match`, and at once when you receive
    [`talent.updated`](/api-reference/webhooks/talent-updated). Never match against an index past
    its `expires_at`.
  </Step>
</Steps>

<Warning>
  The index holds names only. Send a check for every request with a `reference` or `voice_sample`
  input, whatever the local match says. Indirect references ("the runner from Ridgeline's spring
  campaign") are not in the index, and only a check resolves them, so a request you skip goes
  unresolved.
</Warning>

## What can go wrong

| Situation | What to do |
| - | - |
| A check returns `deny` with `ambiguous_talent` and a `disambiguate` remedy | Ask your user who they meant, then send `talent` or `exclude_talent`. |
| A common name matches someone your user did not mean | Use explicit IDs in your interface, or send `exclude_talent` after the user confirms. |
| A resolution returns `status: none` | Nothing was found. That is not a license for any person the output shows. |

Send `exclude_talent` only for a person your user confirmed they did not mean. It is not a way to
remove a talent the prompt names on purpose.

## Related

<Columns cols={2}>
  <Card title="Resolution" icon="magnifying-glass" href="/concepts/resolution">
    How a prompt is matched to talent, and why no match is not permission.
  </Card>

  <Card title="Atlas" icon="diagram-project" href="/concepts/atlas">
    The confirmed names, aliases and credits behind indirect references.
  </Card>

  <Card title="Multi-talent requests" icon="users" href="/build/multi-talent">
    Several talent in one request, and the most restrictive decision.
  </Card>

  <Card title="Resolve talent" icon="code" href="/api-reference/resolutions/resolve-talent">
    The endpoint reference.
  </Card>
</Columns>


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