Skip to main content
Two list routes return more rows than fit in one response: the run list and the usage ledger. They page differently. The run list takes limit and offset; the ledger has no offset, so you move through it by narrowing a time window. Read this page before you export runs or reconcile spend.

At a glance

No route returns a cursor or a total count.

Runs: limit and offset

Any other parameter, or a value that is not a whole number, is refused with 400 invalid_body: “limit and offset must be whole numbers, and no other parameter is accepted”. There is no filter by state, kind or date. The response echoes the limit that was applied, after clamping, and the offset:
A row is the stored run without its answer, usage or cost. replayed is always true, because the row comes from storage. To get the answer of a row, read it with GET /v1/runs/{run_id}. For tokens and spend, use the ledger or GET /v1/usage/runs/{run_id}.

Walk every run

Stop when a page returns fewer rows than limit. Rows are ordered newest first, so a run created while you page pushes older rows one position further: the next page can repeat a row you already have. Deduplicate by run_id.

The ledger: time windows

The ledger has one entry per model call, newest first. It takes a window and a limit, and has no offset:
The ledger writes run_id in the hyphenated form. Both forms name the same run and are accepted by every route. Normalize them before you join ledger entries to run-list rows: remove the hyphens.

Window rules

  • Format: exactly YYYY-MM-DDTHH:MM:SSZ: UTC, second precision, a Z suffix, no fractional seconds and no offset. 2026-09-01T00:00:00Z is accepted; 2026-09-01, 2026-09-01T00:00:00.000Z and 2026-09-01T02:00:00+02:00 are not.
  • Both or neither. Sending one alone is refused. Sending neither means the current quota period, the current UTC calendar month.
  • Half-open: [start_at, end_at). An entry created at exactly end_at is not included, so consecutive windows such as [09-01, 09-02) and [09-02, 09-03) never overlap.
  • Filtered by the call’s time. The ledger filters on when each model call was recorded. The usage totals and the daily series filter on when the run was created, so a run that straddles a boundary can land on different sides.

Walk a window larger than 200 entries

A window returns at most 200 entries, the newest ones. To reach older entries, keep start_at and move end_at down to the oldest created_at you received, then repeat. Timestamps have one-second precision, so several entries can share the oldest second, and some of them may not have fit in the page. Set the next end_at to one second after the oldest created_at, then drop the entries you already have, keyed by run_id and retry_ordinal. Stop when a page brings nothing new.
If more than 200 entries share a single second, this loop cannot get past them. For large exports, walk the period one day at a time instead: send [day, next day) windows, and split a day further if it returns 200 entries. For totals rather than individual entries, GET /v1/usage and GET /v1/usage/daily sum the window for you. See Usage reporting.

Errors

The plain-text 400 has no JSON body. Parse errors defensively, as in Error handling.
  • Usage reporting - totals, the daily series and per-run spend over the same windows.
  • Polling - read one run’s answer after you find it in the list.
  • invalid_parameter - every window and limit refusal, with fixes.
  • Conventions - timestamp, id and money formats across the API.
  • API reference - the list and ledger schemas.