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

# Security

> Store and rotate keys, scope them, act for licensees correctly, verify tokens and webhooks, and report a vulnerability.

The Talent API serves the fictional [sandbox](/sandbox) and three
[live account/license reads](/api-reference/introduction#test-and-live-mode). Sandbox tokens and
receipts do not authorize real talent use. Follow the [quickstart](/quickstart) for the test journey.

Key protection applies to both modes. Sections on connected
licensees, sessions and webhooks describe the security requirements for planned capabilities.

## Secret keys

Test keys start with `rk_test_`; live keys start with `rk_live_`. Both are sent as
`Authorization: Bearer`. Each is a
256-bit secret, shown once when you create it in the [dashboard](https://app.reflections.ai/developers/keys).
Reflections stores only a hash, so a lost key cannot be shown again; create a new one.

* Keep keys on your servers, in a secret manager. Never put one in a web page, a mobile app, a
  browser extension or a repository.
* Use a separate key for each service. Keep live credentials separate from test keys.
* Do not log keys. Redact the `Authorization` header in your request logs.

### Rotate a key

<Steps>
  <Step title="Create the new key">
    Create it in the dashboard with the same scopes.
  </Step>

  <Step title="Deploy it">
    Roll it out to every instance of the service and confirm traffic uses it.
  </Step>

  <Step title="Revoke the old key">
    Revoke it in the dashboard. Requests with it then fail with `401 invalid_api_key`.
  </Step>
</Steps>

If a key leaks, revoke it first and deploy the replacement second. A few minutes of failed checks is
better than a key someone else can use.

## Restricted keys

A restricted key carries only the scopes you give it. A call outside them returns
`403 insufficient_scope`.

| Scope | Lets the key |
| - | - |
| `talent:read` | Read talent, facets and the name index, and resolve talent |
| `licenses:read` | Read licenses and revisions |
| `licenses:write` | Suspend and resume licenses, for a representative |
| `license_requests:write` | Request licenses and withdraw requests, and decline them for a representative |
| `license_drafts:write` | Draft, update and discard licenses, for a representative |
| `mandates:read` | Read your mandates, for a representative |
| `escalations:write` | Read and resolve escalations, for a representative |
| `files:write` | Upload files |
| `checks:write` | Create, read and cancel checks |
| `generations:write` | Create, read and withdraw generations |
| `generations:read` | Read generations |
| `sessions:write` | Create, renew, sample and end sessions |
| `revocations:write` | Read the revocation feed and acknowledge revocations, and revoke for a representative |
| `revocations:read` | Read the revocation feed |
| `usage:write` | Report and read usage |
| `usage:read` | Read usage |
| `statements:read` | Read statements |
| `statements:write` | Record payments, and confirm payments and dispute statements for a representative |
| `webhooks:manage` | Manage webhook endpoints and resend events |
| `events:read` | Read events |

The [authentication reference](/api-reference/authentication#restricted-keys) lists the endpoints
each scope is intended to cover. A scope does not make an unserved endpoint available; see
[Sandbox](/sandbox) for test support and [live reads](/api-reference/introduction#test-and-live-mode)
for the live endpoints. A live license reader needs `licenses:read` only.

Give each service the least it needs. A generation worker needs `files:write`, `checks:write` and
`generations:write`. A revocation worker that only reads the feed needs `revocations:read`; one that acknowledges needs `revocations:write`. Only your admin tooling needs
`webhooks:manage`.

## Acting for licensees

Connected-licensee access is not served in either mode. The API accepts your own account ID
and rejects another account with `account_not_connected`. The following requirements apply to
the planned connection flow.

A platform acts for a connected licensee by sending its account ID in `Reflections-Account`. The
call then runs under that licensee's licenses, within the connection's `scopes` and `licenses`.
Without an active connection that allows the call, it fails with `403 account_not_connected`.

The header is a statement that this request is the licensee's work. Treat it that way:

* Set it from your own record of which licensee the signed-in user acts for. Never take it from a
  request parameter, a form field or anything the user can change.
* Send a licensee's account only for that licensee's work. Running another customer's request, or
  your own, under Ridgeline's account uses Ridgeline's licenses for work they did not commission.
* When you receive [`connection.revoked`](/api-reference/webhooks/connection-revoked), stop sending
  that account at once.

Only Ridgeline's connected platforms can send `acct_2q8v1m3n7p4r6s9t0wxyhk5zba`, and every check
records both the `licensee` and the `platform`.

## Verify tokens

An authorization token (`typ: tlp-auth+jwt`) or a session token (`typ: tlp-session+jwt`) tells a
generation tool what was authorized. The sandbox issues test authorization tokens only; it does
not serve sessions or clear real talent. Verify every token before relying on it in its intended
environment:

<Steps>
  <Step title="Signature">
    ES256, against the key with the token's `kid` in `https://app.reflections.ai/.well-known/jwks.json`.
  </Step>

  <Step title="Header and issuer">
    `typ` is the type you expect, and `iss` is `https://app.reflections.ai`.
  </Step>

  <Step title="Audience">
    `aud` is the platform account you serve. A token issued to another platform fails here.
  </Step>

  <Step title="Time">
    `exp` has not passed and `iat` is not in the future, each with at most 60 seconds of leeway.
  </Step>

  <Step title="Version and obligations">
    `tlp.ver` has major version `1`, and every type in `tlp.obligations` is one you handle.
  </Step>

  <Step title="Model and count">
    For an authorization token, `tlp.execution.model` is the model about to run, and you have run
    fewer than `tlp.max_generations` generations for this `jti`. For a session token,
    `tlp.session` is the session you are running; it carries no count and is replaced by renewal
    every minute.
  </Step>
</Steps>

Any JOSE library can do this. These examples use `jose` for Node.js and `PyJWT` for Python:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // npm install jose
  import { createRemoteJWKSet, jwtVerify } from "jose";

  // Honor the JWKS cache policy and the TLP-TOK-9 refetch limit.
  // The planned 30-day key prepublication policy is not a sandbox service guarantee.
  const JWKS = createRemoteJWKSet(new URL("https://app.reflections.ai/.well-known/jwks.json"), {
    cacheMaxAge: 3_600_000,
    cooldownDuration: 3_600_000,
  });
  // The obligation types of TLP 1.0. Reject a token that carries one you do not handle.
  const OBLIGATIONS = new Set([
    "generation_check", "hold_until_release", "draft_watermark", "generation_instructions",
    "disclosure", "attribution", "c2pa_manifest", "no_training", "delete_material", "usage_report",
  ]);

  type Tlp = {
    ver: string;
    decision: string;
    max_generations: number;
    execution: { provider: string; model: string };
    obligations: string[];
  };

  export async function verifyAuthorization(token: string, model: string) {
    const { payload } = await jwtVerify<{ tlp: Tlp }>(token, JWKS, {
      algorithms: ["ES256"],
      typ: "tlp-auth+jwt",
      issuer: "https://app.reflections.ai",
      audience: "acct_7p4r6s9t0wxyhk5zba2q8v1m3n", // your platform
      clockTolerance: 60,
      maxTokenAge: "15m", // also rejects an iat in the future
      requiredClaims: ["exp", "iat", "jti"],
    });
    const { tlp } = payload;
    if (tlp.ver.split(".")[0] !== "1") throw new Error("unsupported protocol version");
    if (!tlp.obligations.every((type) => OBLIGATIONS.has(type))) throw new Error("unknown obligation");
    if (tlp.execution.model !== model) throw new Error("token is for a different model");
    if (!(await reserve(payload.jti!, 1, tlp.max_generations, payload.exp!))) {
      throw new Error("token used up");
    }
    return payload;
  }
  ```

  ```python Python theme={null}
  import time
  import jwt  # pip install "pyjwt[crypto]"

  JWKS = jwt.PyJWKClient("https://app.reflections.ai/.well-known/jwks.json", lifespan=3600)
  refetch_after = 0.0
  # The obligation types of TLP 1.0. Reject a token that carries one you do not handle.
  OBLIGATIONS = {
      "generation_check", "hold_until_release", "draft_watermark", "generation_instructions",
      "disclosure", "attribution", "c2pa_manifest", "no_training", "delete_material", "usage_report",
  }

  def signing_key(token: str):
      """The token's key. Refetches the JWKS for an unknown kid at most once an hour (TLP-TOK-9)."""
      global refetch_after
      kid = jwt.get_unverified_header(token).get("kid")
      keys = {k.key_id: k.key for k in JWKS.get_signing_keys()}
      if kid not in keys and time.time() >= refetch_after:
          refetch_after = time.time() + 3600
          keys = {k.key_id: k.key for k in JWKS.get_signing_keys(refresh=True)}
      if kid not in keys:
          raise PermissionError("unknown_kid")
      return keys[kid]

  def verify_authorization(token: str, model: str) -> dict:
      if jwt.get_unverified_header(token).get("typ") != "tlp-auth+jwt":
          raise PermissionError("not an authorization token")
      claims = jwt.decode(
          token,
          signing_key(token),
          algorithms=["ES256"],
          issuer="https://app.reflections.ai",
          audience="acct_7p4r6s9t0wxyhk5zba2q8v1m3n",  # your platform
          leeway=60,
          options={"require": ["exp", "iat", "jti"]},  # also rejects an iat in the future
      )
      tlp = claims["tlp"]
      if str(tlp["ver"]).split(".")[0] != "1":
          raise PermissionError("unsupported protocol version")
      if not set(tlp["obligations"]) <= OBLIGATIONS:
          raise PermissionError("unknown obligation")
      if tlp["execution"]["model"] != model:
          raise PermissionError("token is for a different model")
      if not reserve(claims["jti"], 1, tlp["max_generations"], claims["exp"]):
          raise PermissionError("token used up")
      return claims
  ```
</CodeGroup>

`reserve(jti, n, max, exp)` stands for an atomic increment in a store every instance of your
tool shares: it adds `n` to the count for `jti` and returns false if that would pass `max`. Keep
each count until 60 seconds after `exp`, and count a generation you start even if it fails.

### Cache the JWKS

Cache the key set for the `Cache-Control` max-age of its response. When a token names a `kid` you
do not have, refetch once; if it is still unknown, reject the token. The production protocol
requires new keys to be published 30 days before signing, retired public keys to remain available,
and each `kid` to be unique. These are production requirements, not evidence that a sandbox key
has completed that publication period. The current sandbox uses `rfl-test-…` signing key IDs.

## Webhook signatures

Hosted webhook delivery and secret rotation are not served by the current sandbox. The following
is the required verification and rotation contract for the planned service.

Every delivery is signed with your endpoint's `whsec_…` secret under the
[Standard Webhooks](https://www.standardwebhooks.com) scheme. The handler in
[Handle revocations](/build/handling-revocations) shows the full check. The rules:

* Verify against the raw body, before any JSON parsing changes it.
* Compute HMAC-SHA256 with the secret's base64 part over `webhook-id.webhook-timestamp.body`, and
  accept the delivery if it matches any `v1,` entry in `webhook-signature`. Compare in constant
  time.
* Reject a `webhook-timestamp` more than 5 minutes from your clock.
* Use only HTTPS endpoint URLs.

To rotate a secret, call
[`POST /v1/webhook_endpoints/{endpoint_id}/rotate_secret`](/api-reference/webhook-endpoints/rotate-a-webhook-secret).
For 24 hours, deliveries carry signatures from both the old and the new secret, so you can deploy the
new one without dropping events. Rotate at once if a secret leaks.

## Data you send

Send end users as opaque platform IDs, never emails or names, and keep personal data out of
`metadata`. Session tokens may reach a client to open the session stream; secret keys never do. See
[Privacy modes](/build/privacy-modes).

## Report a vulnerability

Email [security@reflections.ai](mailto:security@reflections.ai). Include what you found, the steps
to reproduce it, the request IDs involved, and what an attacker could do with it.

Test only in test mode, with your own account and the sandbox talent. Do not access other
accounts' data, real talent's material or other platforms' outputs, and do not degrade the service
for others. Keep the details private until Reflections has fixed the issue.

<Warning>
  Never send a live secret key, a webhook secret or an end user's personal data in a report. If a key
  was exposed, revoke it in the dashboard before you write.
</Warning>

## Related

<Columns cols={2}>
  <Card title="Authentication" icon="key" href="/api-reference/authentication">
    Keys, scopes, `Reflections-Account` and OAuth.
  </Card>

  <Card title="Authorization token" icon="code" href="/api-reference/formats/authorization-token">
    Every claim of the check token.
  </Card>

  <Card title="Security in the Standard" icon="book" href="/standard/security">
    The threat model and its requirements.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/api-reference/webhooks/overview">
    Signing, retries and replay.
  </Card>
</Columns>


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