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

# Idempotency

> Persist an idempotency key and request body for safe check and generation retries.

Networks fail after a request is sent and before its response arrives. An `Idempotency-Key` lets
you retry a supported mutation without creating a second check or generation. The sandbox's
POST replay and the initial live check/generation custody are described below. Live verification
currently returns a fresh verification ID on each call and does not provide exact response replay;
broader planned POST operations do not have a live implementation.

## Send a key

Every `POST` accepts `Idempotency-Key`, up to 255 characters. Initial live check and generation
creation require a nonempty key. Use a random UUID, created once for each operation and stored
with its exact request body before the first attempt:

This sandbox example uses Bash, cURL and Node.js. Export your own `REFLECTIONS_KEY` as shown in
the [quickstart](/quickstart). Save the following block as `idempotency.sh` in an empty directory,
then run `bash idempotency.sh`. Retry the script from that same directory to reuse the saved key
and body; use a new directory for a new request.

```bash theme={null}
set -euo pipefail
umask 077
: "${REFLECTIONS_KEY:?Export your own test key first}"
case "$REFLECTIONS_KEY" in rk_test_*) ;; *) echo "Use a test key" >&2; exit 1 ;; esac
if [ ! -s idempotency-check.key ]; then
  node -e 'console.log(require("node:crypto").randomUUID())' > idempotency-check.key
fi
if [ ! -s idempotency-check.json ]; then
  cat > idempotency-check.json <<'JSON'
{
  "inputs": [{"role":"prompt","text":"Maya Okafor running a mountain trail at sunrise"}],
  "use": {
    "modality":"image", "kind":"depiction", "purpose":"advertising",
    "commercial":true, "channels":["social"], "territories":["US"]
  },
  "execution":{"provider":"sandbox-fixture","model":"one-pixel-png","candidates":1}
}
JSON
fi
curl -sS --fail-with-body "${REFLECTIONS_ORIGIN:-https://app.reflections.ai}/v1/checks" \
  -H "Authorization: Bearer $REFLECTIONS_KEY" \
  -H "Reflections-Version: 2026-10-15" \
  -H "Idempotency-Key: $(cat idempotency-check.key)" \
  -H "Content-Type: application/json" \
  --data-binary @idempotency-check.json
```

Sandbox keys are kept for 24 hours and scoped to your account and mode. Each key is bound to a
digest of the endpoint and request body. The broader delegated-account contract additionally
scopes keys to the licensee, but delegated `Reflections-Account` requests remain unsupported.

Live check and generation creation each keep a separate account-scoped retry record for 24 hours
from creation, bound to the same caller key and full body. Use a distinct idempotency key for each
operation even though these two live endpoints have separate namespaces.

## Sandbox replay outcomes

| You send | You get |
| - | - |
| The same key and the same body | The original response: the same status, the same body, the same object ID. Nothing runs again. |
| The same key and body while the first request is still running | `409 idempotency_key_in_use`. Retry with the same body and key after `Retry-After`. |
| The same key with a different body or endpoint | `422 idempotency_key_reused`. Nothing runs. |

## Live check and generation replay

Within the same live creation operation, the same body and key replay the admitted object;
a different body with that key returns `422 idempotency_key_reused`. Keep using the same API key.
A pending check returns `202` with `status: "checking"` and `decision: null`; retry with the same
body and key after `Retry-After`. A pending check is not an error.

A completed check replay returns the same sealed response, including its `decision` and
`authorization`; it does not use another generation from `max_generations`. Read the check's GET
endpoint for current authorization consumption or expiry.

In the initial live image implementation, generation creation seals an accepted `202` response.
Every successful POST replay returns that same response even after evaluation or release.
Use `GET /v1/generations/{generation_id}` for its current state and final receipt. A POST replay
does not rerun the evaluator, submit another candidate or release the output.

Retry `429`, `500`, `503`, `409 idempotency_key_in_use` and a `202` check that is still
`checking` with the same body and key after `Retry-After`. These transient responses are not final outcomes. A retry resumes or reconciles
the same logical operation; it never authorizes another paid evaluation start. Unknown provider
spend is not automatically retried or refunded.

## Retry with the same key

When you do not know whether a request succeeded (a timeout, a dropped connection, a `500` or a
`503`), retry with the same body and key. Never generate a new key for a retry: a new key is a new request.

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function postWithRetry(path: string, body: unknown, key = crypto.randomUUID()) {
    const deadline = Date.now() + 300_000;
    for (let attempt = 0; attempt < 5; attempt++) {
      let res: Response;
      try {
        res = await fetch(`https://app.reflections.ai/v1${path}`, {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
            "Reflections-Version": "2026-10-15",
            "Idempotency-Key": key,
            "Content-Type": "application/json",
          },
          body: JSON.stringify(body),
          signal: AbortSignal.timeout(10_000),
        });
      } catch {
        await sleep(backoff(attempt));
        continue; // Unknown outcome: retry with the same key.
      }
      if (res.ok) {
        const result = await res.json();
        // A live check still evaluating is not final: send the same body and key after Retry-After.
        if (res.status === 202 && result.object === "check" && result.status === "checking") {
          if (Date.now() > deadline) throw new Error("The check is still evaluating: do not generate a named talent");
          await sleep(retryAfterMs(res) ?? 1000);
          attempt--; // Waiting for a decision does not use up a retry.
          continue;
        }
        return result;
      }
      const problem = await res.json().catch(() => ({})); // a proxy's 502 may not be JSON
      const retryable =
        res.status === 429 || res.status >= 500 || problem.code === "idempotency_key_in_use";
      if (!retryable) throw new Error(`${res.status} ${problem.code}: ${problem.detail ?? problem.title}`);
      await sleep(retryAfterMs(res) ?? backoff(attempt));
    }
    throw new Error("Reflections unavailable: do not generate a named talent");
  }

  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
  const backoff = (attempt: number) => Math.min(250 * 2 ** attempt, 4000) * (0.5 + Math.random());

  // Retry-After from Reflections is in seconds; a proxy in front of it may send an HTTP date.
  function retryAfterMs(res: Response): number | undefined {
    const value = res.headers.get("Retry-After");
    if (!value) return undefined;
    const seconds = Number(value);
    if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
    const date = Date.parse(value);
    return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
  }
  ```

  ```python Python theme={null}
  import os, random, time, uuid, requests
  from email.utils import parsedate_to_datetime

  def post_with_retry(path, body, key=None):
      key = key or str(uuid.uuid4())
      deadline = time.monotonic() + 300
      attempt = 0
      while attempt < 5:
          try:
              res = requests.post(
                  f"https://app.reflections.ai/v1{path}",
                  headers={
                      "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
                      "Reflections-Version": "2026-10-15",
                      "Idempotency-Key": key,
                  },
                  json=body,
                  timeout=10,
              )
          except requests.RequestException:
              time.sleep(backoff(attempt))
              attempt += 1
              continue  # Unknown outcome: retry with the same key.
          if res.ok:
              result = res.json()
              # A live check still evaluating is not final: send the same body and key after Retry-After.
              if res.status_code == 202 and result.get("object") == "check" and result.get("status") == "checking":
                  if time.monotonic() > deadline:
                      raise RuntimeError("The check is still evaluating: do not generate a named talent")
                  wait = retry_after(res)
                  time.sleep(1 if wait is None else wait)
                  continue  # Waiting for a decision does not use up a retry.
              return result
          try:
              problem = res.json()
          except ValueError:  # a proxy's 502 may not be JSON
              problem = {}
          retryable = res.status_code == 429 or res.status_code >= 500 or problem.get("code") == "idempotency_key_in_use"
          if not retryable:
              raise RuntimeError(f"{res.status_code} {problem.get('code')}: {problem.get('detail', problem.get('title'))}")
          wait = retry_after(res)
          time.sleep(backoff(attempt) if wait is None else wait)
          attempt += 1
      raise RuntimeError("Reflections unavailable: do not generate a named talent")

  def backoff(attempt):
      return min(0.25 * 2 ** attempt, 4) * (0.5 + random.random())

  def retry_after(res):
      """Seconds to wait, or None. Reflections sends seconds; a proxy may send an HTTP date."""
      value = res.headers.get("Retry-After")
      if not value:
          return None
      try:
          return max(0.0, float(value))
      except ValueError:
          pass
      try:
          return max(0.0, parsedate_to_datetime(value).timestamp() - time.time())
      except (TypeError, ValueError):
          return None
  ```
</CodeGroup>

If the retries run out, treat the outcome as unknown. For a check, do not generate. For a
generation, do not deliver, and retry later with the same key. A later retry within 24 hours still
returns the original result if the first attempt completed.
If the 24-hour retry window has elapsed while the outcome is still unknown, reconcile the
existing operation before submitting anything new. Expiry does not prove that no work occurred.

## Keys across calls

Use one key per operation, not per call site. A check and the generation that follows it are two
operations with two keys. In the planned [usage-record contract](/api-reference/usage-records/report-usage),
a batch is one operation: keep the same key and the same records when you retry it. That contract
also rejects duplicate usage records with `duplicate`; live usage submission remains unsupported.

## Related

<Columns cols={2}>
  <Card title="Errors" icon="ban" href="/api-reference/errors">
    Which codes are safe to retry.
  </Card>

  <Card title="Caching and outages" icon="bolt" href="/build/caching-and-outages">
    What to do when Reflections cannot be reached.
  </Card>
</Columns>


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