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

# Report usage

> Report outputs, seconds, characters, placements and impressions in batches, read reconciliation, and send corrections.

<Note>
  Planned protocol reference. Usage records, reconciliation, statements and payment recording
  are not served by the current Talent API sandbox. The examples below cannot yet be run against
  it.
  Start with the [runnable quickstart](/quickstart) and [sandbox support matrix](/sandbox).
</Note>

Licenses that pay the talent per output, per second or per impression are settled from the usage
you report. Reflections reconciles your reports against its own record of checks and generations,
and issues a monthly statement per license. Reflections never moves money; the licensee pays the
talent's payee and records the payment.

## Know what to report

A `usage_report` obligation on a check, generation or session says which unit to report and how
soon:

```json theme={null}
{ "type": "usage_report", "units": "seconds", "within": "P1D" }
```

Report each subject within `within`, counted from the generation's decision or the session's end.
Each record names one `subject`: the generation (`gen_…`) or session (`ses_…`) it settles.

| `units` | Report | Typical subject |
| - | - | - |
| `outputs` | Delivered outputs, usually `1` per generation | Generation |
| `seconds` | Seconds of generated audio or video, or of a live session | Generation or session |
| `characters` | Characters of generated text or of a speech script | Generation |
| `placements` | Places the output was published, with `placement` | Generation |
| `impressions` | Times the output was shown, with `placement` | Generation |

The account that knows the number reports it. The platform reports outputs, seconds and
characters. Placements and impressions are usually known to the licensee, who reports them, or the
platform does when it publishes for the licensee. Send `Reflections-Account` when you report for a
connected licensee.

A record counts against one license. When a generation or session was decided under several
licenses, as with two talent in one output, send one record per license and set `license` on each.
With one license, `license` is optional.

## Send a batch

Send up to 1,000 records per call to
[`POST /v1/usage_records`](/api-reference/usage-records/report-usage). Give each batch its own
`Idempotency-Key`, derived from your batch ID, so a retry never counts twice.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.reflections.ai/v1/usage_records \
    -H "Authorization: Bearer rk_test_…" \
    -H "Reflections-Version: 2026-10-15" \
    -H "Idempotency-Key: usage-batch-2026-10-17T18" \
    -H "Content-Type: application/json" \
    -d '{
      "records": [
        { "subject": "ses_q8v1m3n7p4r6s9t0wxyhk5zba2", "units": "seconds", "quantity": 412, "occurred_at": "2026-10-17T18:07:00Z" },
        { "subject": "gen_2q8v1m3n7p4r6s9t0wxyhk5zba", "units": "seconds", "quantity": 3.4, "occurred_at": "2026-10-17T18:06:03Z" },
        {
          "subject": "gen_6s9t0wxyhk5zba2q8v1m3n7p4r",
          "units": "placements",
          "quantity": 1,
          "occurred_at": "2026-10-17T19:00:00Z",
          "placement": { "channel": "social", "territory": "US", "url": "https://social.example.com/ridgeline/posts/1182" }
        }
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  type UsageItem = {
    subject: string; // gen_… or ses_…
    units: string;
    quantity: number;
    occurredAt: string;
    placement?: { channel: string; territory: string; url?: string };
  };

  // One batch from your usage store. Its ID never changes, so a retry reuses the key.
  export async function reportUsage(batch: { id: string; items: UsageItem[] }) {
    const res = await fetch("https://app.reflections.ai/v1/usage_records", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.REFLECTIONS_KEY}`,
        "Reflections-Version": "2026-10-15",
        "Idempotency-Key": `usage-batch-${batch.id}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        records: batch.items.map((item) => ({
          subject: item.subject,
          units: item.units,
          quantity: item.quantity,
          occurred_at: item.occurredAt,
          ...(item.placement ? { placement: item.placement } : {}),
        })),
      }),
    });
    if (!res.ok) throw new Error(`Report usage failed: ${res.status}`); // retry with the same key
    const result = await res.json();
    return result.data.filter((r: { status: string }) => r.status === "rejected");
  }
  ```

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

  def report_usage(batch):
      """batch: one batch from your usage store. Its ID never changes, so a retry reuses the key."""
      res = requests.post(
          "https://app.reflections.ai/v1/usage_records",
          headers={
              "Authorization": f"Bearer {os.environ['REFLECTIONS_KEY']}",
              "Reflections-Version": "2026-10-15",
              "Idempotency-Key": f"usage-batch-{batch.id}",
          },
          json={
              "records": [
                  {
                      "subject": item.subject,  # gen_… or ses_…
                      "units": item.units,
                      "quantity": item.quantity,
                      "occurred_at": item.occurred_at,
                      **({"placement": item.placement} if item.placement else {}),
                  }
                  for item in batch.items
              ]
          },
      )
      res.raise_for_status()  # retry with the same key
      return [r for r in res.json()["data"] if r["status"] == "rejected"]

  rejected = [r for r in result["data"] if r["status"] == "rejected"]
  ```
</CodeGroup>

Each record is accepted or rejected on its own. The response lists them in the order you sent:

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "id": "ur_6a8c0e2g4j6m8p0r2t4w6y8b0d",
      "object": "usage_record",
      "subject": "ses_q8v1m3n7p4r6s9t0wxyhk5zba2",
      "units": "seconds",
      "quantity": 412,
      "occurred_at": "2026-10-17T18:07:00Z",
      "status": "accepted",
      "rejection": null,
      "license": "lic_ba2q8v1m3n7p4r6s9t0wxyhk5z",
      "statement": "stmt_3d5f7h9k1m3p5r7t9w1y3a5c7e"
    },
    {
      "id": "ur_8c0e2g4j6m8p0r2t4w6y8b0d2f",
      "object": "usage_record",
      "subject": "gen_2q8v1m3n7p4r6s9t0wxyhk5zba",
      "units": "seconds",
      "quantity": 3.4,
      "status": "rejected",
      "rejection": { "code": "duplicate", "detail": "ur_4y6a8c0e2g4j6m8p0r2t4w6y8b already reports this generation in seconds." },
      "license": "lic_ba2q8v1m3n7p4r6s9t0wxyhk5z",
      "statement": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## Handle rejections

| `rejection.code` | Meaning | What to do |
| - | - | - |
| `unknown_subject` | The ID is not a generation or session of yours, or of the licensee you named. | Check the ID and the `Reflections-Account` header. |
| `exceeds_observed` | More units than Reflections recorded, such as more seconds than the session ran. | Report what you measured from the output. |
| `outside_scope` | The placement's channel or territory is outside the license. | Do not retry. Remove the placement, and tell the licensee. |
| `duplicate` | This subject, license and unit are already reported. | Nothing, unless the first record was wrong; then correct it. |
| `period_closed` | The statement for that period is already issued. | Contact [support](/production/support) with the record IDs. |

An `outside_scope` placement means the output was used where the license does not cover it. The
rejection does not undo the placement; take it down.

## Correct a record

Records never change. To fix one, send a new record with `correction_of` set to the record it
replaces. The new record replaces the old one in reconciliation.

```json theme={null}
{
  "records": [
    {
      "subject": "ses_q8v1m3n7p4r6s9t0wxyhk5zba2",
      "units": "seconds",
      "quantity": 398,
      "occurred_at": "2026-10-17T18:07:00Z",
      "correction_of": "ur_6a8c0e2g4j6m8p0r2t4w6y8b0d"
    }
  ]
}
```

## Reconciliation and statements

Reflections records what it observed: every allowed generation, every session's duration, every
script's length. Each statement line shows `reported` (yours), `observed` (Reflections') and
`billable`, the greater of the two. Under-reporting is visible without trusting either side.

```json theme={null}
{
  "id": "stmt_3d5f7h9k1m3p5r7t9w1y3a5c7e",
  "object": "statement",
  "license": "lic_ba2q8v1m3n7p4r6s9t0wxyhk5z",
  "status": "issued",
  "period": { "start": "2026-10-01T00:00:00Z", "end": "2026-11-01T00:00:00Z" },
  "currency": "usd",
  "lines": [
    {
      "description": "Speaking voice, live sessions",
      "units": "seconds",
      "reported": 182400,
      "observed": 183150,
      "billable": 183150,
      "rate": { "amount": 2, "currency": "usd" },
      "amount": 366300
    }
  ],
  "amount_due": 366300,
  "amount_paid": 0,
  "reconciliation": { "result": "under_reported", "detail": "750 seconds observed and not reported." }
}
```

`reconciliation.result` is `matched`, `under_reported` or `over_reported`. When a statement is
final for the month, the licensee receives
[`statement.issued`](/api-reference/webhooks/statement-issued). The licensee pays the talent's
payee directly, then records it with
[`POST /v1/statements/{statement_id}/payments`](/api-reference/statements/record-a-payment). The
statement becomes `paid` when confirmed payments reach `amount_due`, or `disputed` if the
talent's side disputes it.

Daily test statements are a proposed test capability. The current sandbox does not generate
statements or accept usage records.

## What can go wrong

| Problem | What to do |
| - | - |
| A batch call times out | Retry with the same `Idempotency-Key`. A new key would send every record again, and duplicates are rejected one by one. |
| `422 idempotency_key_reused` | You changed the batch and kept its key. A changed batch is a new batch. |
| You missed `within` | Report as soon as you can. The deadline is part of the license you are using. |

<Note>
  Reported usage, statements and payments are kept for 7 years, so the talent and their
  representatives can audit every license.
</Note>

## Related

<Columns cols={2}>
  <Card title="Usage and settlement" icon="scale-balanced" href="/concepts/usage-and-settlement">
    Usage records, reconciliation, statements and audit.
  </Card>

  <Card title="Report usage" icon="code" href="/api-reference/usage-records/report-usage">
    The endpoint reference.
  </Card>

  <Card title="Brands and agencies" icon="briefcase" href="/guides/brands-and-agencies">
    Reading statements and recording payments.
  </Card>

  <Card title="Real-time sessions" icon="microphone" href="/build/real-time-sessions">
    Ending a session and reporting its seconds.
  </Card>
</Columns>


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