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

# Video integration design

> Planned video workflow: check a use, upload an output and receive a decision by webhook.

<Note>
  Planned design guide. This workflow requires file uploads and webhooks, which are not served
  by the current Talent API sandbox. The examples below describe the target protocol and cannot
  yet be run end to end.
  Start with the [runnable quickstart](/quickstart) and [sandbox support matrix](/sandbox).
</Note>

The target video workflow checks outputs asynchronously: submit the output, receive
`status: "checking"`, then handle the decision by webhook. The examples use the fictional talent
Maya Okafor, whose fixture license covers video for social media and the web in the US and Canada.
Uploads and webhook delivery must be implemented before this full workflow can run.

<Steps>
  <Step title="Export your test key">
    ```bash theme={null}
    export REFLECTIONS_KEY="rk_test_…"
    ```
  </Step>

  <Step title="Create a webhook endpoint">
    The decision on a video arrives as an event, so register an HTTPS endpoint first. To receive
    test events on your machine, point it at a tunnel to `localhost`.

    <CodeGroup>
      ```bash cURL theme={null}
      curl https://app.reflections.ai/v1/webhook_endpoints \
        -H "Authorization: Bearer $REFLECTIONS_KEY" \
        -H "Reflections-Version: 2026-10-15" \
        -H "Idempotency-Key: $(uuidgen)" \
        -H "Content-Type: application/json" \
        -d '{
          "url": "https://example.com/webhooks/reflections",
          "events": ["generation.allowed", "generation.denied", "generation.released", "generation.expired"],
          "description": "Video generation decisions"
        }'
      ```

      ```typescript TypeScript theme={null}
      const api = "https://app.reflections.ai/v1";
      const auth = {
        Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
        "Reflections-Version": "2026-10-15",
      };

      async function ok(res: Response) {
        if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); // the problem details
        return res.json();
      }

      const endpoint = await fetch(`${api}/webhook_endpoints`, {
        method: "POST",
        headers: { ...auth, "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json" },
        body: JSON.stringify({
          url: "https://example.com/webhooks/reflections",
          events: ["generation.allowed", "generation.denied", "generation.released", "generation.expired"],
          description: "Video generation decisions",
        }),
      }).then(ok);
      ```

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

      API = "https://app.reflections.ai/v1"
      auth = {
          "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
          "Reflections-Version": "2026-10-15",
      }

      def ok(res):
          if not res.ok:
              raise RuntimeError(f"{res.status_code} {res.text}")  # the problem details
          return res.json()

      endpoint = ok(requests.post(
          f"{API}/webhook_endpoints",
          headers={**auth, "Idempotency-Key": str(uuid.uuid4())},
          json={
              "url": "https://example.com/webhooks/reflections",
              "events": ["generation.allowed", "generation.denied", "generation.released", "generation.expired"],
              "description": "Video generation decisions",
          },
      ))
      ```
    </CodeGroup>

    The response carries the signing secret once. Store it as `REFLECTIONS_WEBHOOK_SECRET`.

    ```json theme={null}
    {
      "id": "we_rbnd8r31pq37ny6qtrt3ebaxqa",
      "object": "webhook_endpoint",
      "livemode": false,
      "url": "https://example.com/webhooks/reflections",
      "events": ["generation.allowed", "generation.denied", "generation.released", "generation.expired"],
      "status": "enabled",
      "api_version": "2026-10-15",
      "secret": "whsec_…"
    }
    ```
  </Step>

  <Step title="Check the use">
    The check works as it does for images, with `modality: "video"`.

    <CodeGroup>
      ```bash cURL theme={null}
      CHECK=$(curl -sS --fail-with-body https://app.reflections.ai/v1/checks \
        -H "Authorization: Bearer $REFLECTIONS_KEY" \
        -H "Reflections-Version: 2026-10-15" \
        -H "Idempotency-Key: $(uuidgen)" \
        -H "Content-Type: application/json" \
        -d '{
          "inputs": [
            { "role": "prompt", "text": "Maya Okafor running a ridge trail at dawn, slow tracking shot, 30 seconds" }
          ],
          "use": {
            "modality": "video",
            "kind": "depiction",
            "purpose": "advertising",
            "commercial": true,
            "channels": ["social"],
            "territories": ["US"]
          },
          "end_user": { "id": "usr_48213" },
          "execution": { "provider": "example-video-provider", "model": "video-model-2", "candidates": 1 }
        }')
      echo "$CHECK" | jq '{decision, reasons}'
      CHECK_ID=$(echo "$CHECK" | jq -r .id)
      ```

      ```typescript TypeScript theme={null}
      const check = await fetch(`${api}/checks`, {
        method: "POST",
        headers: { ...auth, "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json" },
        body: JSON.stringify({
          inputs: [
            { role: "prompt", text: "Maya Okafor running a ridge trail at dawn, slow tracking shot, 30 seconds" },
          ],
          use: {
            modality: "video",
            kind: "depiction",
            purpose: "advertising",
            commercial: true,
            channels: ["social"],
            territories: ["US"],
          },
          end_user: { id: "usr_48213" },
          execution: { provider: "example-video-provider", model: "video-model-2", candidates: 1 },
        }),
      }).then(ok);
      if (check.decision !== "allow") {
        throw new Error(check.reasons.map((r: { sentence: string }) => r.sentence).join(" "));
      }
      ```

      ```python Python theme={null}
      check = ok(requests.post(
          f"{API}/checks",
          headers={**auth, "Idempotency-Key": str(uuid.uuid4())},
          json={
              "inputs": [
                  {"role": "prompt", "text": "Maya Okafor running a ridge trail at dawn, slow tracking shot, 30 seconds"}
              ],
              "use": {
                  "modality": "video",
                  "kind": "depiction",
                  "purpose": "advertising",
                  "commercial": True,
                  "channels": ["social"],
                  "territories": ["US"],
              },
              "end_user": {"id": "usr_48213"},
              "execution": {"provider": "example-video-provider", "model": "video-model-2", "candidates": 1},
          },
      ))
      if check["decision"] != "allow":
          raise SystemExit(" ".join(r["sentence"] for r in check["reasons"]))
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "id": "chk_x59t0jzzp857rbn5z93cz93r44",
      "object": "check",
      "livemode": false,
      "status": "allowed",
      "decision": "allow",
      "obligations": [
        { "type": "generation_check", "required": "all" },
        {
          "type": "generation_instructions",
          "instructions": ["Keep Maya Okafor the same person: face, skin tone and build unchanged.", "Do not show alcohol or tobacco."]
        },
        { "type": "disclosure", "level": "licensed", "placement": "both", "text": "Made with AI. Likeness used under license." },
        { "type": "c2pa_manifest" }
      ],
      "authorization": {
        "token": "eyJhbGciOiJFUzI1NiIs…",
        "max_generations": 1,
        "generations_used": 0,
        "expires_at": "2026-10-17T18:15:00Z"
      }
    }
    ```

    Generate the video with the `generation_instructions` added to the model's input, and send the
    provider `authorization.token`. Start the job before `authorization.expires_at`.
  </Step>

  <Step title="Create the file and get an upload URL">
    Multipart uploads take files up to 100 MB. For longer videos, create the file with JSON: its
    size, its SHA-256 and its media type. Files up to 5 GB are accepted this way.

    <CodeGroup>
      ```bash cURL theme={null}
      SIZE=$(wc -c < output.mp4 | tr -d ' ')
      SHA=$(shasum -a 256 output.mp4 | cut -d' ' -f1)

      FILE=$(curl -sS --fail-with-body https://app.reflections.ai/v1/files \
        -H "Authorization: Bearer $REFLECTIONS_KEY" \
        -H "Reflections-Version: 2026-10-15" \
        -H "Idempotency-Key: $(uuidgen)" \
        -H "Content-Type: application/json" \
        -d "{
          \"purpose\": \"output\",
          \"size\": $SIZE,
          \"sha256\": \"$SHA\",
          \"media_type\": \"video/mp4\",
          \"filename\": \"output.mp4\"
        }")
      FILE_ID=$(echo "$FILE" | jq -r .id)
      UPLOAD_URL=$(echo "$FILE" | jq -r .upload_url)
      ```

      ```typescript TypeScript theme={null}
      import { createReadStream } from "node:fs";
      import { stat } from "node:fs/promises";
      import { createHash } from "node:crypto";

      async function sha256File(path: string): Promise<string> {
        const hash = createHash("sha256");
        for await (const chunk of createReadStream(path)) hash.update(chunk);
        return hash.digest("hex");
      }

      const { size } = await stat("output.mp4");
      const sha256 = await sha256File("output.mp4");

      const file = await fetch(`${api}/files`, {
        method: "POST",
        headers: { ...auth, "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json" },
        body: JSON.stringify({ purpose: "output", size, sha256, media_type: "video/mp4", filename: "output.mp4" }),
      }).then(ok);
      ```

      ```python Python theme={null}
      import hashlib

      def sha256_file(path: str) -> str:
          h = hashlib.sha256()
          with open(path, "rb") as f:
              for chunk in iter(lambda: f.read(1024 * 1024), b""):
                  h.update(chunk)
          return h.hexdigest()

      size = os.path.getsize("output.mp4")
      sha256 = sha256_file("output.mp4")

      file = ok(requests.post(
          f"{API}/files",
          headers={**auth, "Idempotency-Key": str(uuid.uuid4())},
          json={"purpose": "output", "size": size, "sha256": sha256, "media_type": "video/mp4", "filename": "output.mp4"},
      ))
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "id": "file_asxwsxs1rajf9z0bwwdvf1a0fd",
      "object": "file",
      "livemode": false,
      "purpose": "output",
      "status": "uploading",
      "sha256": "0cab1c9617404faf2b24e221e189ca5945813e14d3f766345b09ca13bbe28ffc",
      "media_type": "video/mp4",
      "size": 187654321,
      "upload_url": "https://uploads.reflections.ai/file_asxwsxs1rajf9z0bwwdvf1a0fd?expires=1792303380&signature=bf44f3ded5ea89a97712f4cfa69df1432a3f7b85ca0",
      "expires_at": "2026-10-18T18:03:00Z"
    }
    ```

    `upload_url` expires in 1 hour.
  </Step>

  <Step title="Upload the bytes">
    `PUT` the file to `upload_url`, then read the file until its `status` is `ready`.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -sS --fail-with-body --upload-file output.mp4 -H "Content-Type: video/mp4" "$UPLOAD_URL"

      curl -sS --fail-with-body "https://app.reflections.ai/v1/files/$FILE_ID" \
        -H "Authorization: Bearer $REFLECTIONS_KEY" \
        -H "Reflections-Version: 2026-10-15" | jq -r .status
      ```

      ```typescript TypeScript theme={null}
      import { Readable } from "node:stream";

      const put = await fetch(file.upload_url, {
        method: "PUT",
        headers: { "Content-Type": "video/mp4", "Content-Length": String(size) },
        body: Readable.toWeb(createReadStream("output.mp4")),
        duplex: "half",
      });
      if (!put.ok) throw new Error(`Upload failed: ${put.status}`);

      let ready = file;
      while (ready.status === "uploading") {
        await new Promise((resolve) => setTimeout(resolve, 2000));
        ready = await fetch(`${api}/files/${file.id}`, { headers: auth }).then(ok);
      }
      ```

      ```python Python theme={null}
      import time

      with open("output.mp4", "rb") as f:
          requests.put(file["upload_url"], data=f, headers={"Content-Type": "video/mp4"}).raise_for_status()

      ready = file
      while ready["status"] == "uploading":
          time.sleep(2)
          ready = ok(requests.get(f"{API}/files/{file['id']}", headers=auth))
      ```
    </CodeGroup>

    If the status is `expired`, the upload did not complete in time. Create a new file.
  </Step>

  <Step title="Submit the output">
    Name the check and the candidate, and give the video's duration. In test mode, put
    `rfl_test_async` in `transcript` to run the asynchronous path on a fixed timer: the decision
    arrives 10 seconds later.

    <CodeGroup>
      ```bash cURL theme={null}
      printf '{
        "check": "%s",
        "candidate_index": 0,
        "artifact": { "file": "%s", "sha256": "%s", "media_type": "video/mp4", "duration_ms": 30000 },
        "execution": { "provider": "example-video-provider", "model": "video-model-2", "job_id": "job_77120" },
        "transcript": "rfl_test_async"
      }' "$CHECK_ID" "$FILE_ID" "$SHA" |
      curl -sS --fail-with-body https://app.reflections.ai/v1/generations \
        -H "Authorization: Bearer $REFLECTIONS_KEY" \
        -H "Reflections-Version: 2026-10-15" \
        -H "Idempotency-Key: $(uuidgen)" \
        -H "Content-Type: application/json" \
        --data-binary @-
      ```

      ```typescript TypeScript theme={null}
      const generation = await fetch(`${api}/generations`, {
        method: "POST",
        headers: { ...auth, "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json" },
        body: JSON.stringify({
          check: check.id,
          candidate_index: 0,
          artifact: { file: file.id, sha256, media_type: "video/mp4", duration_ms: 30000 },
          execution: { provider: "example-video-provider", model: "video-model-2", job_id: "job_77120" },
          transcript: "rfl_test_async",
        }),
      }).then(ok);
      ```

      ```python Python theme={null}
      generation = ok(requests.post(
          f"{API}/generations",
          headers={**auth, "Idempotency-Key": str(uuid.uuid4())},
          json={
              "check": check["id"],
              "candidate_index": 0,
              "artifact": {"file": file["id"], "sha256": sha256, "media_type": "video/mp4", "duration_ms": 30000},
              "execution": {"provider": "example-video-provider", "model": "video-model-2", "job_id": "job_77120"},
              "transcript": "rfl_test_async",
          },
      ))
      ```
    </CodeGroup>

    The generation has no decision yet:

    ```json theme={null}
    {
      "id": "gen_x5ey3x2r1vxpf1drv6v6aftnx4",
      "object": "generation",
      "livemode": false,
      "check": "chk_x59t0jzzp857rbn5z93cz93r44",
      "candidate_index": 0,
      "status": "checking",
      "decision": null,
      "artifact": {
        "file": "file_asxwsxs1rajf9z0bwwdvf1a0fd",
        "sha256": "0cab1c9617404faf2b24e221e189ca5945813e14d3f766345b09ca13bbe28ffc",
        "media_type": "video/mp4",
        "duration_ms": 30000
      },
      "receipt": null,
      "c2pa": null,
      "decided_at": null
    }
    ```

    Keep the video private while `status` is `checking`. In live mode the p95 target for a video
    decision is half its duration plus 5 seconds: 20 seconds for this one.
  </Step>

  <Step title="Receive the decision">
    Verify the signature on every delivery before you act on it. Deliveries follow the
    [Standard Webhooks](https://www.standardwebhooks.com) scheme.

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

      const secret = Buffer.from(process.env.REFLECTIONS_WEBHOOK_SECRET!.slice("whsec_".length), "base64");

      function verifyWebhook(headers: Headers, body: string): boolean {
        const id = headers.get("webhook-id");
        const timestamp = headers.get("webhook-timestamp");
        const signatures = headers.get("webhook-signature");
        if (!id || !timestamp || !signatures) return false;
        if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

        const expected = createHmac("sha256", secret).update(`${id}.${timestamp}.${body}`).digest();
        return signatures.split(" ").some((entry) => {
          const [version, signature] = entry.split(",");
          if (version !== "v1" || !signature) return false;
          const received = Buffer.from(signature, "base64");
          return received.length === expected.length && timingSafeEqual(received, expected);
        });
      }

      const seen = new Set<string>();

      export async function POST(request: Request): Promise<Response> {
        const body = await request.text();
        if (!verifyWebhook(request.headers, body)) return new Response(null, { status: 400 });

        const event = JSON.parse(body);
        if (seen.has(event.id)) return new Response(null, { status: 200 });
        seen.add(event.id);

        const generation = event.data.object;
        if (event.type === "generation.allowed" || event.type === "generation.released") {
          // Embed generation.c2pa in the video's C2PA manifest, show the disclosure, deliver.
        } else if (event.type === "generation.denied" || event.type === "generation.expired") {
          // Never deliver. generation.reasons say why.
        }
        return new Response(null, { status: 200 });
      }
      ```

      ```python Python theme={null}
      import base64, hashlib, hmac, json, os, time

      secret = base64.b64decode(os.environ["REFLECTIONS_WEBHOOK_SECRET"].removeprefix("whsec_"))
      seen: set[str] = set()

      def verify_webhook(headers: dict, body: bytes) -> bool:
          msg_id = headers.get("webhook-id")
          timestamp = headers.get("webhook-timestamp")
          signatures = headers.get("webhook-signature")
          if not (msg_id and timestamp and signatures):
              return False
          if abs(time.time() - int(timestamp)) > 300:
              return False
          signed = f"{msg_id}.{timestamp}.".encode() + body
          expected = base64.b64encode(hmac.new(secret, signed, hashlib.sha256).digest()).decode()
          return any(
              entry.startswith("v1,") and hmac.compare_digest(entry[3:], expected)
              for entry in signatures.split(" ")
          )

      def handle_webhook(headers: dict, body: bytes) -> int:
          """Call this from your web framework's route. Returns the HTTP status to send."""
          if not verify_webhook(headers, body):
              return 400
          event = json.loads(body)
          if event["id"] in seen:
              return 200
          seen.add(event["id"])

          generation = event["data"]["object"]
          if event["type"] in ("generation.allowed", "generation.released"):
              pass  # Embed generation["c2pa"] in the video's C2PA manifest, show the disclosure, deliver.
          elif event["type"] in ("generation.denied", "generation.expired"):
              pass  # Never deliver. generation["reasons"] say why.
          return 200
      ```
    </CodeGroup>

    About 10 seconds after you submitted, the endpoint receives `generation.allowed`:

    ```json theme={null}
    {
      "id": "evt_1vs6rzzxpm9mathg8ddm8xrfbz",
      "object": "event",
      "type": "generation.allowed",
      "api_version": "2026-10-15",
      "livemode": false,
      "sequence": 2,
      "created_at": "2026-10-17T18:04:12Z",
      "data": {
        "object": {
          "id": "gen_x5ey3x2r1vxpf1drv6v6aftnx4",
          "object": "generation",
          "status": "allowed",
          "decision": "allow",
          "assurance": "observed",
          "receipt": "rcpt_fd31n3jj9tn9y15yf16869ps6d",
          "c2pa": {
            "label": "ai.reflections.license",
            "data": {
              "receipt": "eyJhbGciOiJFUzI1NiIs…",
              "verify_url": "https://app.reflections.ai/r/rcpt_fd31n3jj9tn9y15yf16869ps6d"
            }
          },
          "decided_at": "2026-10-17T18:04:12Z"
        },
        "previous_attributes": { "status": "checking", "decision": null }
      }
    }
    ```

    Return a `2xx` within 10 seconds and do slow work afterwards. Deliveries can repeat and can
    arrive out of order: skip an `id` you have handled, and ignore an event whose `sequence` is
    lower than one you processed for the same generation.
  </Step>

  <Step title="Embed the receipt and deliver">
    Add `c2pa` to the video's C2PA manifest as an assertion, with the standard `c2pa.actions`
    entry, sign it as your platform, and show the disclosure text. See the
    [manifest definition](/build/embedding-provenance#write-the-manifest); [Embedding provenance](/build/embedding-provenance)
    covers MP4.

    If you missed the event, read the generation with
    [`GET /v1/generations/{generation_id}`](/api-reference/generations/retrieve-a-generation), or
    resend it with [`POST /v1/events/{event_id}/resend`](/api-reference/events/resend-an-event).
  </Step>
</Steps>

## What can go wrong

| Response | Cause | What to do |
| - | - | - |
| `409 check_expired` | The authorization expired before you used it | Run a new check, then start the job |
| `422 artifact_digest_mismatch` | The uploaded bytes do not match the `sha256` you declared | Hash the exact file you upload |
| `generation.denied` with `prohibited_motion` or `narration_mismatch` | A rule failed on the video | Discard it. `reasons[].sentence` says which rule |
| `generation.expired` | A held output was not released within its window | Discard it |
| `webhook_endpoint.disabled` | Your endpoint failed every delivery for 5 days | Fix it, re-enable it, and read missed events with [`GET /v1/events`](/api-reference/events/list-events) |

If the license holds outputs for release, the generation returns `held` instead of `checking`,
and the decision arrives as `generation.released` or `generation.denied`. The
[image quickstart](/quickstarts/image) runs that path with Jonah Reyes.

## Next

<Columns cols={2}>
  <Card title="Image and video platforms" icon="video" href="/guides/image-and-video-platforms">
    The check and generation calls in a production pipeline.
  </Card>

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

  <Card title="Submitting outputs" icon="upload" href="/build/submitting-outputs">
    Uploads, candidates, digests and findings.
  </Card>

  <Card title="Embedding provenance" icon="stamp" href="/build/embedding-provenance">
    Writing the C2PA assertion and the disclosure label.
  </Card>
</Columns>


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