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

# Authentication

> Secret keys, restricted scopes, acting for a licensee, and connecting licensees with OAuth.

Use a test key for the fictional sandbox or a live key for the three
[live account/license reads](/api-reference/introduction#test-and-live-mode). Discovery, JWKS and
receipt retrieval are public. Verification looks only in the caller's mode; see
[the verification table](#endpoints-that-need-no-key). Connections and
Talent API OAuth below describe the planned contract and cannot be used yet.

## Secret keys

Send your key as a bearer token:

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

| Prefix | Mode |
| - | - |
| `rk_test_` | Test mode. Runs against the [sandbox](/sandbox). |
| `rk_live_` | Live account details, the licenses the account holds or issued, and the state of outputs under them. Live checks and output submission return `403 live_not_activated` until activation. |

Choose Test or Live when creating a key in the [dashboard](https://app.reflections.ai/developers/keys).
A key is a 256-bit
secret, shown once when you create it. Reflections stores only a hash, so a lost key cannot be
shown again; create a new one. To rotate a key, create the new key, deploy it, then revoke the old
one in the dashboard. A revoked key stops working at once and cannot be restored.

Keep keys on your server. Never put a key in a browser, a mobile app, a prompt or a model's
context. A client that follows a live session connects to the
[session stream](/api-reference/realtime/session-stream) with the session token instead.

| Problem | Response |
| - | - |
| No `Authorization` header | `401 authentication_required` |
| Unknown or revoked key | `401 invalid_api_key` |
| Restricted key without the scope | `403 insufficient_scope` |
| Live key on a test-only authenticated endpoint | `403 livemode_mismatch` |

## Restricted keys

A restricted key can call only the endpoints its scopes cover. Give each service the scopes it needs:
the supported inline generation flow needs `checks:write` and `generations:write`. Test helpers
require an unrestricted key. A scope name does not mean its planned endpoint is implemented.
For live mode, `licenses:read` covers only license list/retrieve; revision and material access
remain planned. `generations:read` covers the live output list for a license. `GET /v1/account` works with any valid key.

| Scope | Endpoints |
| - | - |
| `talent:read` | [Talent](/api-reference/talent/list-talent), the [name index](/api-reference/talent/download-the-name-index), [facets](/api-reference/facets/list-talent-facets), [resolutions](/api-reference/resolutions/resolve-talent) |
| `licenses:read` | [Licenses](/api-reference/licenses/list-licenses) and their revisions, [licensed material](/api-reference/facets/retrieve-licensed-material) |
| `licenses:write` | [Suspend](/api-reference/licenses/suspend-a-license) and [resume](/api-reference/licenses/resume-a-license) licenses, for a representative |
| `license_requests:write` | [License requests](/api-reference/license-requests/request-a-license): create, list, retrieve, withdraw, and [decline](/api-reference/license-requests/decline-a-license-request) for a representative |
| `license_drafts:write` | [License drafts](/api-reference/license-drafts/draft-a-license): create, list, retrieve, update, discard |
| `mandates:read` | [Mandates](/api-reference/mandates/list-mandates): list and retrieve |
| `escalations:write` | [Escalations](/api-reference/escalations/list-escalations): list, retrieve, resolve |
| `files:write` | [Files](/api-reference/files/upload-a-file): upload and retrieve |
| `checks:write` | [Checks](/api-reference/checks/create-a-check): create, list, retrieve, cancel |
| `generations:write` | [Generations](/api-reference/generations/create-a-generation): create, list, retrieve, withdraw |
| `generations:read` | [Generations](/api-reference/generations/list-generations): list and retrieve |
| `sessions:write` | [Sessions](/api-reference/sessions/create-a-session): create, retrieve, renew, samples, end |
| `revocations:write` | [Revocations](/api-reference/revocations/list-revocations): the feed, retrieve, acknowledge, and [create](/api-reference/revocations/create-a-revocation) for a representative |
| `revocations:read` | The [revocation feed](/api-reference/revocations/list-revocations) |
| `usage:write` | [Usage records](/api-reference/usage-records/report-usage): report and list |
| `usage:read` | [Usage records](/api-reference/usage-records/list-usage-records): list |
| `statements:read` | [Statements](/api-reference/statements/list-statements): list and retrieve |
| `statements:write` | [Record a payment](/api-reference/statements/record-a-payment), [confirm a payment](/api-reference/statements/confirm-a-payment), [dispute a statement](/api-reference/statements/dispute-a-statement) |
| `webhooks:manage` | [Webhook endpoints](/api-reference/webhook-endpoints/create-a-webhook-endpoint), and [resending events](/api-reference/events/resend-an-event) |
| `events:read` | [Events](/api-reference/events/list-events): list and retrieve |

`GET /v1/account` works with any key. Connections and the test helpers need an unrestricted key. A call without the scope fails with:

```json theme={null}
{
  "type": "https://docs.reflections.ai/errors/insufficient_scope",
  "title": "The key lacks a required scope",
  "status": 403,
  "code": "insufficient_scope",
  "detail": "This key does not have checks:write.",
  "request_id": "req_8f2k4m6p8r0t2v4x6z8b0d2f4h"
}
```

## Endpoints that need no key

[Retrieve a receipt](/api-reference/receipts/retrieve-a-receipt), discovery and JWKS need no key. A
receipt's unguessable ID grants access to its public JSON. Talent API OAuth metadata is planned.

[Verification](/api-reference/verifications/verify-content) searches only the caller's mode. A
request with no key searches live receipts:

| Verify | Test receipt | Live receipt |
| - | - | - |
| Receipt ID or JWS | Test key | No key, or a live key |
| SHA-256 digest | Test key of the account that submitted it | Live key of the account that submitted it |

A receipt from the other mode returns `200` with `result: "not_found"`, not an error. Do not read
`not_found` as proof that a receipt does not exist until you have checked with the right key.

## Planned: act for a licensee

A license belongs to its licensee, such as Ridgeline. When a platform such as Northlight runs the
generation, it calls with its own secret key and names the licensee in `Reflections-Account`:

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/checks \
    -H "Authorization: Bearer rk_test_…" \
    -H "Reflections-Version: 2026-10-15" \
    -H "Reflections-Account: acct_2q8v1m3n7p4r6s9t0wxyhk5zba" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "inputs": [{ "role": "prompt", "text": "Maya Okafor lacing a trail shoe at the trailhead" }],
      "use": {
        "modality": "image", "kind": "depiction", "purpose": "advertising",
        "commercial": true, "channels": ["social"], "territories": ["US"]
      }
    }'
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch("https://app.reflections.ai/v1/checks", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
      "Reflections-Version": "2026-10-15",
      "Reflections-Account": "acct_2q8v1m3n7p4r6s9t0wxyhk5zba",
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      inputs: [{ role: "prompt", text: "Maya Okafor lacing a trail shoe at the trailhead" }],
      use: {
        modality: "image", kind: "depiction", purpose: "advertising",
        commercial: true, channels: ["social"], territories: ["US"],
      },
    }),
  });
  ```

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

  res = requests.post(
      "https://app.reflections.ai/v1/checks",
      headers={
          "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
          "Reflections-Version": "2026-10-15",
          "Reflections-Account": "acct_2q8v1m3n7p4r6s9t0wxyhk5zba",
          "Idempotency-Key": str(uuid.uuid4()),
      },
      json={
          "inputs": [{"role": "prompt", "text": "Maya Okafor lacing a trail shoe at the trailhead"}],
          "use": {
              "modality": "image", "kind": "depiction", "purpose": "advertising",
              "commercial": True, "channels": ["social"], "territories": ["US"],
          },
      },
  )
  ```
</CodeGroup>

The header is valid only while an active [connection](/api-reference/connections/list-connections)
links the two accounts and its `scopes` include what the call needs. Otherwise the call fails with
`403 account_not_connected`. The check records both sides: `licensee` is Ridgeline and `platform`
is Northlight, and the receipt names both.

Endpoints that accept the header list it on their reference page. Webhook endpoints and events do
not: to receive events about a connected licensee's objects, create the endpoint with
`connected: true` (see [Webhooks](/api-reference/webhooks/overview#connected-account-events)).

## Planned: connect a licensee with OAuth

A connection is created when the licensee approves your platform through OAuth 2.0 authorization
code with PKCE (`S256`). The [authorization server metadata](/api-reference/oauth/retrieve-the-authorization-server-metadata)
is at `https://app.reflections.ai/.well-known/oauth-authorization-server`.

<Steps>
  <Step title="Read your client ID">
    [`GET /v1/account`](/api-reference/account/retrieve-your-account) returns `platform.client_id`
    and the `platform.display_name` the licensee will see. Register your redirect URI in the
    [dashboard](https://app.reflections.ai/developers).
  </Step>

  <Step title="Send the licensee to the authorization page">
    Create a code verifier, keep it with the user's session, and redirect to the
    [authorization page](/api-reference/oauth/authorize-a-client),
    `https://app.reflections.ai/oauth/authorize`:

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

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

      const codeVerifier = randomBytes(32).toString("base64url");
      const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
      const state = randomBytes(16).toString("base64url");

      const url = new URL("https://app.reflections.ai/oauth/authorize");
      url.search = new URLSearchParams({
        response_type: "code",
        client_id: account.platform.client_id,
        redirect_uri: "https://northlight.example/oauth/reflections/callback",
        scope: "files:write checks:write generations:write usage:write",
        state,
        code_challenge: codeChallenge,
        code_challenge_method: "S256",
      }).toString();
      // Store codeVerifier and state with the user's session, then redirect to url.
      ```

      ```python Python theme={null}
      import base64, hashlib, os, secrets, requests
      from urllib.parse import urlencode

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

      code_verifier = secrets.token_urlsafe(32)
      code_challenge = (
          base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
          .rstrip(b"=")
          .decode()
      )
      state = secrets.token_urlsafe(16)

      url = "https://app.reflections.ai/oauth/authorize?" + urlencode({
          "response_type": "code",
          "client_id": account["platform"]["client_id"],
          "redirect_uri": "https://northlight.example/oauth/reflections/callback",
          "scope": "files:write checks:write generations:write usage:write",
          "state": state,
          "code_challenge": code_challenge,
          "code_challenge_method": "S256",
      })
      # Store code_verifier and state with the user's session, then redirect to url.
      ```
    </CodeGroup>

    `scope` is a space-separated list of the connection scopes: `talent:read`, `licenses:read`,
    `license_requests:write`, `files:write`, `checks:write`, `generations:write`,
    `sessions:write`, `revocations:write`, `usage:write` and `statements:read`. Ask only for what
    you use.
  </Step>

  <Step title="The licensee approves">
    The licensee signs in to Reflections and sees your `display_name`, the scopes, and a choice of
    licenses: all of them, or the ones they list. If they decline, your redirect URI receives
    `error=access_denied`.
  </Step>

  <Step title="Create the connection">
    Your redirect URI receives `code`, `state` and `iss`. Check that `state` matches and that `iss`
    is `https://app.reflections.ai`, then redeem the code
    from your server with
    [`POST /v1/connections`](/api-reference/connections/create-a-connection), your secret key and
    the code verifier, within 10 minutes:

    <CodeGroup>
      ```bash cURL theme={null}
      curl https://app.reflections.ai/v1/connections \
        -H "Authorization: Bearer $REFLECTIONS_KEY" \
        -H "Reflections-Version: 2026-10-15" \
        -H "Idempotency-Key: connection-$STATE" \
        -H "Content-Type: application/json" \
        -d '{
          "code": "ac_Jf3k9QmZ2vX8pT4wR7nY1bL6",
          "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
          "redirect_uri": "https://northlight.example/oauth/reflections/callback"
        }'
      ```

      ```typescript TypeScript theme={null}
      // In your redirect handler, after checking state. codeVerifier is from the user's session.
      export async function createConnection(code: string, codeVerifier: string, state: string) {
        const res = await fetch("https://app.reflections.ai/v1/connections", {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
            "Reflections-Version": "2026-10-15",
            "Idempotency-Key": `connection-${state}`, // A retry redeems the code once.
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            code,
            code_verifier: codeVerifier,
            redirect_uri: "https://northlight.example/oauth/reflections/callback",
          }),
        });
        if (!res.ok) throw new Error(`Create connection failed: ${res.status} ${await res.text()}`);
        return res.json(); // The connection; its `licensee` is the account you act for.
      }
      ```

      ```python Python theme={null}
      def create_connection(code, code_verifier, state):
          """In your redirect handler, after checking state. code_verifier is from the user's session."""
          res = requests.post(
              "https://app.reflections.ai/v1/connections",
              headers={
                  "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
                  "Reflections-Version": "2026-10-15",
                  "Idempotency-Key": f"connection-{state}",  # A retry redeems the code once.
              },
              json={
                  "code": code,
                  "code_verifier": code_verifier,
                  "redirect_uri": "https://northlight.example/oauth/reflections/callback",
              },
              timeout=10,
          )
          res.raise_for_status()
          return res.json()  # The connection; its "licensee" is the account you act for.
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "id": "con_av0baahbfheftkzxqbkns02qwf",
      "object": "connection",
      "livemode": true,
      "status": "active",
      "licensee": "acct_2q8v1m3n7p4r6s9t0wxyhk5zba",
      "licensee_name": "Ridgeline",
      "platform": "acct_7p4r6s9t0wxyhk5zba2q8v1m3n",
      "platform_name": "Northlight",
      "scopes": ["files:write", "checks:write", "generations:write", "usage:write"],
      "licenses": { "mode": "all", "ids": [] },
      "revoked_by": null,
      "revoked_at": null,
      "created_at": "2026-09-29T10:12:00Z"
    }
    ```

    No access token is issued: the code proves the licensee approved, and your secret key proves
    who you are. A code works once. An unknown, expired or used code, or a `code_verifier` or
    `redirect_uri` that does not match, returns `400`
    [`authorization_code_invalid`](/api-reference/errors#authorization_code_invalid); send the
    licensee through the authorization page again. Your account also receives
    [`connection.created`](/api-reference/webhooks/connection-created).
  </Step>

  <Step title="Call for the licensee">
    Send your own secret key with `Reflections-Account` set to the connection's `licensee`.
  </Step>
</Steps>

Either side can end a connection with
[`DELETE /v1/connections/{connection_id}`](/api-reference/connections/end-a-connection). You receive
[`connection.revoked`](/api-reference/webhooks/connection-revoked), and calls for that licensee
fail with `account_not_connected` from that moment.

Agents connect to the MCP server with OAuth 2.1 instead: they
[register](/api-reference/oauth/register-a-client) and receive an access token from
[`/oauth/token`](/api-reference/oauth/create-an-access-token). See [MCP](/api-reference/mcp/overview).

## Act for a talent

A representative, such as an agency or a manager, calls with its own key and names the talent in
the path or the body. No header is needed: the [mandate](/api-reference/mandates/list-mandates) the
talent gave your account decides what you may do, power by power. A call the mandate does not
cover fails with `403 mandate_required`, and every call fails that way once the mandate ends. See
[Talent representatives](/guides/talent-representatives).

## Related

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

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


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