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

# Handle escalations

> Show a pending state, wait for check.resolved or poll, cancel what you no longer need, and keep interactive products off the review queue.

<Note>
  Sandbox and protocol reference. Test escalations can be resolved with the sandbox helper and
  read by polling. Webhook delivery and a production human review queue are not available
  through this API.
  Start with the [runnable quickstart](/quickstart) and [sandbox support matrix](/sandbox).
</Note>

`escalate` means a person with authority has to decide before anything is generated or delivered.
The response comes back at once; nobody waits on an open connection. Your product shows a pending
state, learns the answer from a webhook or by polling, and acts on it.

## What an escalation is

At the check, `escalate` means: do not generate yet. At the generation, it means: the output is
held, and you deliver nothing until a reviewer releases it. Both carry an `escalation` object.

```json theme={null}
{
  "id": "chk_r6s9t0wxyhk5zba2q8v1m3n7p4",
  "object": "check",
  "status": "escalated",
  "decision": "escalate",
  "reasons": [
    {
      "code": "prohibited_custom_term",
      "stage": "check",
      "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
      "term": "license:lic_k5zba2q8v1m3n7p4r6s9t0wxyh/terms.neverExtras",
      "sentence": "Do not show alcohol or tobacco."
    }
  ],
  "authorization": null,
  "escalation": {
    "id": "esc_t0wxyhk5zba2q8v1m3n7p4r6s9",
    "reason": "judgment",
    "reviewer": "talent",
    "status": "open",
    "expires_at": "2026-10-24T18:04:00Z",
    "resolved_at": null,
    "note": null
  }
}
```

| `escalation.reason` | Why a person decides |
| - | - |
| `judgment` | A rule the talent wrote needs a person to read it against this request. |
| `unverifiable` | A protected attribute could not be confirmed from the request. |
| `evaluation_unavailable` | The check could not finish evaluating. |
| `release_review` | The license has every output released by a person. Generations only. |

`reviewer` says who decides. Under a customer license it is the talent, or a representative with
the `review_outputs` power. Under a platform license it is Reflections trust and safety, with a
response target of one business day.

A missing license never escalates. It is a `deny` with `no_license` and a `license_request` remedy.

## Show a pending state

<Steps>
  <Step title="Store the check ID with the job">
    Keep `check.id` and `escalation.expires_at` next to your user's job, so a webhook or a poll can
    find it later.
  </Step>

  <Step title="Tell the user what is happening">
    Say the request is waiting for approval and show the reason sentence as written. Show when it
    expires. Do not show a draft or a partial result: nothing has been generated.
  </Step>

  <Step title="Freeze the request">
    The answer covers exactly the request you escalated. If your user edits it, cancel the check
    and send a new one.
  </Step>
</Steps>

## Listen for `check.resolved`

When the reviewer answers, or the escalation expires, your endpoint receives
[`check.resolved`](/api-reference/webhooks/check-resolved) with the check as it is now.

| `decision` in the event | What to do |
| - | - |
| `allow` | Generate. `authorization` now covers exactly the escalated request, bound by `request_digest`. Meet every obligation. |
| `deny` | Do not generate. Show `reasons[].sentence`. The check's `status` is `denied`, with `review_declined` when the reviewer declined or `hold_expired` when the escalation expired. |

The authorization in a resolved check has its own `expires_at`, like any other. Start the
generation when the event arrives. `escalation.note` holds the reviewer's note, if they wrote one.

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Call after verifying the webhook signature (see Handle revocations).
  export async function onEvent(event: { type: string; data: { object: any } }) {
    if (event.type !== "check.resolved") return;
    const check = event.data.object;
    const job = await jobs.findByCheck(check.id);
    if (!job || job.state !== "pending") return; // duplicate or cancelled

    if (check.decision === "allow") {
      await jobs.start(job.id, { token: check.authorization.token, obligations: check.obligations });
    } else {
      await jobs.fail(job.id, check.reasons.map((r: { sentence: string }) => r.sentence));
    }
  }
  ```

  ```python Python theme={null}
  # Call after verifying the webhook signature (see Handle revocations).
  def on_event(event):
      if event["type"] != "check.resolved":
          return
      check = event["data"]["object"]
      job = jobs.find_by_check(check["id"])
      if not job or job.state != "pending":
          return  # duplicate or cancelled

      if check["decision"] == "allow":
          jobs.start(job.id, token=check["authorization"]["token"], obligations=check["obligations"])
      else:
          jobs.fail(job.id, [r["sentence"] for r in check["reasons"]])
  ```
</CodeGroup>

`jobs` stands for your own job store. Webhooks arrive at least once, so make the handler safe to
run twice.

## Poll as a fallback

If you cannot receive webhooks, or one is late, read the check with
[`GET /v1/checks/{check_id}`](/api-reference/checks/retrieve-a-check). Poll at most once every 5
seconds while `status` is `escalated`, and slow down for escalations that take hours.

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

  ```typescript TypeScript theme={null}
  async function waitForCheck(id: string) {
    let delay = 5_000; // never poll faster than every 5 seconds
    for (;;) {
      const check = await fetch(`https://app.reflections.ai/v1/checks/${id}`, {
        headers: {
          Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
          "Reflections-Version": "2026-10-15",
        },
      }).then((r) => r.json());
      if (check.status !== "escalated") return check;
      await new Promise((resolve) => setTimeout(resolve, delay));
      delay = Math.min(delay * 2, 300_000);
    }
  }
  ```

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

  def wait_for_check(check_id):
      delay = 5  # never poll faster than every 5 seconds
      while True:
          check = requests.get(
              f"https://app.reflections.ai/v1/checks/{check_id}",
              headers={
                  "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
                  "Reflections-Version": "2026-10-15",
              },
          ).json()
          if check["status"] != "escalated":
              return check
          time.sleep(delay)
          delay = min(delay * 2, 300)
  ```
</CodeGroup>

## Cancel what you no longer need

When your user leaves, deletes the job or edits the request, cancel the check. The reviewer is
told, and the check's `status` becomes `cancelled`.

```bash theme={null}
curl -X POST https://app.reflections.ai/v1/checks/chk_r6s9t0wxyhk5zba2q8v1m3n7p4/cancel \
  -H "Authorization: Bearer rk_test_…" \
  -H "Reflections-Version: 2026-10-15" \
  -H "Idempotency-Key: $(uuidgen)"
```

If the reviewer answered first, the call returns `409 escalation_closed`. Read the check and act on
its decision, or discard it. You can also cancel an allowed check to void its authorization early.

## Interactive products: `on_escalate: "deny"`

A chat reply or a live voice call cannot wait days for a person. Send `on_escalate: "deny"` on
every check from an interactive surface. Anything that would escalate returns `deny` at once:

```json theme={null}
{
  "status": "denied",
  "decision": "deny",
  "reasons": [
    {
      "code": "review_required",
      "stage": "check",
      "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s",
      "sentence": "This request needs Maya Okafor's approval before it can be made."
    }
  ],
  "escalation": null,
  "remedy": { "type": "retry_with_hold", "talent": "tal_9t0wxyhk5zba2q8v1m3n7p4r6s" }
}
```

The `retry_with_hold` remedy tells you the request could go to review. Offer that in a surface
where waiting makes sense, such as a saved draft, and send it again with `on_escalate: "hold"`.
[Sessions](/build/real-time-sessions) always behave this way.

## Expiry

An escalation stays open for 7 days (`P7D`), or for the license's shorter `escalation_window`.
After `escalation.expires_at`, the check's `status` becomes `denied`, with decision `deny` and the
reason `hold_expired`, and you receive `check.resolved`.

A check whose `status` is `expired` is a different case: it was allowed, and its authorization
expired before you used it. Run a new check if you still need the output.

## Held outputs at the generation

When a license has a person release every output, the generation returns `decision: "escalate"`,
`status: "held"` and `escalation.reason: "release_review"`.

* Keep the output private. If the generation carries a `draft_watermark` obligation, you may show
  a preview with the visible draft mark. Otherwise show nothing.
* On [`generation.released`](/api-reference/webhooks/generation-released), the generation carries
  its receipt. Embed it and deliver the clean output.
* On [`generation.denied`](/api-reference/webhooks/generation-denied), discard it. Its status is
  `denied`, with `review_declined` when the reviewer declined it, or `license_revoked` when the
  license was revoked before release.
* On [`generation.expired`](/api-reference/webhooks/generation-expired), discard it. Nobody released
  it in time: its status is `expired`, its decision `deny` and its reason `review_expired`. Held
  outputs expire after 7 days.
* If your user no longer wants a held output,
  [withdraw it](/api-reference/generations/withdraw-a-generation). Its delivery returns to the
  license's delivery limit.

## Test it

In test mode, put `rfl_test_escalate`, `rfl_test_unverifiable` or `rfl_test_unavailable` in a text
input to force each kind of escalation. Generations for Jonah Reyes
(`tal_4n6p8r0t2v4x6z8b0d2f4h6j8k`) are held. Answer any of them with
[`POST /v1/test_helpers/escalations/{escalation_id}/resolve`](/api-reference/test-helpers/resolve-an-escalation),
using `allow`, `deny` or `expire`.

## Related

<Columns cols={2}>
  <Card title="Escalation" icon="gavel" href="/concepts/escalation">
    Who reviews, how long it takes, and what expiry means.
  </Card>

  <Card title="check.resolved" icon="webhook" href="/api-reference/webhooks/check-resolved">
    The event and its payload.
  </Card>

  <Card title="Submit outputs" icon="image" href="/build/submitting-outputs">
    Holds, releases and findings at the generation.
  </Card>

  <Card title="Sandbox" icon="flask" href="/sandbox">
    Force every escalation in test mode.
  </Card>
</Columns>


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