> ## 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 and reads of the licenses the calling account holds as licensee. 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); 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.

# Quickstart

> Run a test check, submit a small PNG, and verify its signed receipt without calling a model.

Run the supported sandbox journey with your own test key. This uses a one-pixel PNG fixture, so
there is no model call, provider charge or real talent. Sandbox decisions are simulated: a signed
test receipt does not clear a real person's likeness or prove the image depicts the fixture.

You need **Bash, cURL, jq and Node.js 22 or later**. The API reference also includes native
TypeScript, Python and Go HTTP examples; no Reflections SDK package is required.

## 1. Get a test key

Create an **unrestricted test key** in the
[dashboard](https://app.reflections.ai/developers/keys). The escalation helper needs an unrestricted
key. Keep it private and export it in your terminal:

```bash theme={null}
export REFLECTIONS_KEY="rk_test_…"
export REFLECTIONS_ORIGIN="https://app.reflections.ai"
```

## 2. Check, submit and verify

Save this block as `quickstart.sh`, then run `bash quickstart.sh` from the same terminal. It stops
on an HTTP failure, malformed response, a check without `allow`, or a generation without `allow`.
It writes request bodies and one idempotency key per logical request to `reflections-demo`.
Rerun it with that directory unchanged after an unknown network outcome. For a new request, export
a new `REFLECTIONS_DEMO_DIR`; never change a body while reusing its key.

```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
export REFLECTIONS_ORIGIN="${REFLECTIONS_ORIGIN:-https://app.reflections.ai}"
export REFLECTIONS_DEMO_DIR="${REFLECTIONS_DEMO_DIR:-reflections-demo}"
mkdir -p "$REFLECTIONS_DEMO_DIR"
cd "$REFLECTIONS_DEMO_DIR"

# A retry reads the same key file for each logical request.
for operation in check generation verification; do
  if [ ! -s "$operation.key" ]; then
    node -e 'console.log(require("node:crypto").randomUUID())' > "$operation.key"
  fi
done
if [ ! -s check-request.json ]; then
  cat > check-request.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/v1/checks" \
  -H "Authorization: Bearer $REFLECTIONS_KEY" \
  -H "Reflections-Version: 2026-10-15" \
  -H "Idempotency-Key: $(cat check.key)" \
  -H "Content-Type: application/json" \
  --data-binary @check-request.json > check.json
jq -e '.livemode == false and .decision == "allow" and (.id | type == "string")' check.json > /dev/null
CHECK_ID=$(jq -er .id check.json)
jq -r '"Check \(.id): \(.decision), talent \([.talent[].name] | join(", "))"' check.json

# The digest and base64 come from exactly the same PNG bytes.
if [ ! -s generation-request.json ]; then
  node --input-type=module - "$CHECK_ID" <<'JS'
import { createHash } from "node:crypto";
import { writeFileSync } from "node:fs";
const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADUlEQVQImWP4////fwAJ+wP9CNHoHgAAAABJRU5ErkJggg==", "base64");
writeFileSync("output.png", png);
writeFileSync("generation-request.json", JSON.stringify({
  check: process.argv[2], candidate_index: 0,
  artifact: {data: png.toString("base64"), sha256: createHash("sha256").update(png).digest("hex"), media_type: "image/png"}
}));
JS
fi
curl -sS --fail-with-body "$REFLECTIONS_ORIGIN/v1/generations" \
  -H "Authorization: Bearer $REFLECTIONS_KEY" \
  -H "Reflections-Version: 2026-10-15" \
  -H "Idempotency-Key: $(cat generation.key)" \
  -H "Content-Type: application/json" \
  --data-binary @generation-request.json > generation.json
jq -e '.livemode == false and .decision == "allow" and (.receipt | type == "string")' generation.json > /dev/null
RECEIPT_ID=$(jq -er .receipt generation.json)
jq -r '"Generation \(.id): \(.decision), receipt \(.receipt)"' generation.json

curl -sS --fail-with-body "$REFLECTIONS_ORIGIN/v1/receipts/$RECEIPT_ID" > receipt.json
jq -e '.livemode == false and .decision == "allow" and (.jws | type == "string")' receipt.json > /dev/null
curl -sS --fail-with-body "$REFLECTIONS_ORIGIN/.well-known/jwks.json" > jwks.json
if [ ! -s verification-request.json ]; then
  jq '{receipt: .jws}' receipt.json > verification-request.json
fi
curl -sS --fail-with-body "$REFLECTIONS_ORIGIN/v1/verifications" \
  -H "Authorization: Bearer $REFLECTIONS_KEY" \
  -H "Reflections-Version: 2026-10-15" \
  -H "Idempotency-Key: $(cat verification.key)" \
  -H "Content-Type: application/json" \
  --data-binary @verification-request.json > verification.json
jq -e '.result == "verified" and .method == "receipt" and .receipt.livemode == false' verification.json > /dev/null
jq -r '"Verification \(.id): \(.result) by \(.method)"' verification.json
printf 'Verified test receipt: %s/v1/receipts/%s\n' "$REFLECTIONS_ORIGIN" "$RECEIPT_ID"
```

Inline media is limited to **7 MiB (7,340,032 decoded bytes)** and the entire generation JSON body
to **10 MiB (10,485,760 bytes)**, including base64 overhead. `/files`, multipart verification,
manifest verification and media evaluation by a real model are not supported. See [Sandbox](/sandbox).

## 3. Verify the signature locally

Run this from your original directory. It checks the JWS against the published test JWKS, the
pinned issuer, receipt identity, test key ID and original artifact digest. This is a test-mode
example; do not use it to accept real talent content.

```bash theme={null}
node --input-type=module <<'JS'
import assert from "node:assert/strict";
import { createHash, createPublicKey, verify } from "node:crypto";
import { readFileSync } from "node:fs";
const dir = process.env.REFLECTIONS_DEMO_DIR || "reflections-demo";
const json = (name) => JSON.parse(readFileSync(`${dir}/${name}`, "utf8"));
const receipt = json("receipt.json");
const [header64, payload64, signature64, extra] = receipt.jws.split(".");
assert(header64 && payload64 && signature64 && extra === undefined);
const header = JSON.parse(Buffer.from(header64, "base64url").toString());
const claims = JSON.parse(Buffer.from(payload64, "base64url").toString());
assert.equal(header.alg, "ES256");
assert.equal(header.typ, "tlp-receipt+jwt");
assert.equal(header.crit, undefined);
for (const field of ["jwk", "jku", "x5u", "x5c"]) assert.equal(header[field], undefined);
assert.match(header.kid, /^rfl-test-/);
const key = json("jwks.json").keys.find((key) => key.kid === header.kid);
assert(key && key.kty === "EC" && key.crv === "P-256" && key.alg === "ES256");
assert(verify("sha256", Buffer.from(`${header64}.${payload64}`), {
  key: createPublicKey({key, format: "jwk"}), dsaEncoding: "ieee-p1363"
}, Buffer.from(signature64, "base64url")));
assert.equal(receipt.livemode, false);
assert.equal(claims.iss, process.env.REFLECTIONS_ORIGIN || "https://app.reflections.ai");
assert.equal(claims.jti, receipt.id);
assert.equal(claims.sub, json("generation.json").id);
assert.equal(claims.tlp.kind, "generation");
assert.equal(claims.tlp.decision, "allow");
assert.equal(claims.tlp.ver, "1.0");
assert(Number.isFinite(claims.iat) && claims.iat <= Date.now() / 1000 + 60);
assert.equal(claims.tlp.artifact_sha256, createHash("sha256").update(readFileSync(`${dir}/output.png`)).digest("hex"));
console.log("Test JWS signature, issuer, receipt and artifact digest verified.");
JS
```

Signature verification proves what this issuer signed. To read its current status, retrieve the
receipt again. JSON verification also accepts `{ "receipt": "rcpt_…" }` or
`{ "sha256": "<the original file's hex digest>" }`; use a new persisted key for each new logical
verification. The sandbox does not fetch URLs or extract C2PA manifests.

## 4. Exercise the stop paths

Use the same check body with **Iris Calder** or **Dev Marlow** in place of Maya. Each returns
`deny`. Add `rfl_test_escalate` to Maya's prompt to get `escalate`. Save each changed body in a
new demo directory with new keys. The script stops before submission in all three cases. A `200`
alone is never permission to proceed.

<Accordion title="A deny response (Iris Calder)">
  The request:

  ```json theme={null}
  {
    "inputs": [{"role":"prompt","text":"Iris Calder on a beach"}],
    "use": {
      "modality":"image", "kind":"depiction", "purpose":"advertising",
      "commercial":true, "channels":["social"], "territories":["US"]
    },
    "execution":{"provider":"sandbox-fixture","model":"one-pixel-png","candidates":1}
  }
  ```

  The response:

  ```json theme={null}
  {
    "id": "chk_nt35gw168wjzs4pph48x0msna7",
    "object": "check",
    "livemode": false,
    "status": "denied",
    "decision": "deny",
    "talent": [
      {
        "talent": "tal_7q9s1v3w5y7a9c1e3g5h7k9m1n",
        "name": "Iris Calder",
        "matched_by": "name",
        "confidence": "high",
        "decision": "deny",
        "facets": [
          "fct_r9drsnmda2wta72hvsdeb0gfvk"
        ],
        "license": null,
        "license_revision": null,
        "terms_digest": null,
        "escalation": null
      }
    ],
    "use": {
      "modality": "image",
      "kind": "depiction",
      "purpose": "advertising",
      "commercial": true,
      "channels": [
        "social"
      ],
      "territories": [
        "US"
      ]
    },
    "reasons": [
      {
        "code": "no_license",
        "term": "talent:tal_7q9s1v3w5y7a9c1e3g5h7k9m1n",
        "sentence": "Iris Calder requires a license for this use, and you hold none.",
        "stage": "check",
        "talent": "tal_7q9s1v3w5y7a9c1e3g5h7k9m1n"
      }
    ],
    "obligations": [],
    "authorization": null,
    "escalation": null,
    "remedy": {
      "type": "license_request",
      "talent": "tal_7q9s1v3w5y7a9c1e3g5h7k9m1n",
      "suggested_scope": {
        "modalities": [
          "image"
        ],
        "kinds": [
          "depiction"
        ],
        "purposes": [
          "advertising"
        ],
        "commercial": true,
        "channels": [
          "social"
        ],
        "territories": [
          "US"
        ],
        "facet_types": [
          "likeness"
        ]
      }
    },
    "request_digest": "sha256:b6685c954290df922cefd1a4347bb00ca92299bfd665346afb13338c986266b1",
    "licensee": "acct_s2mnrgt1jkvbhk6b0rzcq9qn82",
    "platform": "acct_s2mnrgt1jkvbhk6b0rzcq9qn82",
    "session": null,
    "receipt": "rcpt_dfq0ay8w9vs3bm0vmszffm837t",
    "metadata": {},
    "created_at": "2026-10-19T17:45:24Z"
  }
  ```

  Branch on `reasons[].code`. `remedy` tells the licensee what would change the answer.
</Accordion>

Jonah Reyes's check allows the use, but his output stays `held` with `decision: "escalate"`.
The script stops after saving that generation. [Resolve its escalation](/sandbox#resolve-an-escalation)
and retrieve the generation again; continue only when it returns `allow`. For asynchronous test
outputs, poll at most once every five seconds. Webhook delivery is not implemented in the sandbox.

## Next

* [Sandbox](/sandbox): supported operations, fixtures, stop paths and limits.
* [API reference](/api-reference/introduction): native HTTP samples and per-operation availability.
* [Production status](/production/conformance): what remains before live generation.


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