Usage

Query token usage and spend across your organization and projects.

Read-only, and the most-used group in practice: what was consumed, when, and by what. Usage is a view of the same usage records the portal's Analytics and Logs pages and your invoice use. The API computes nothing of its own, so a late correction appears everywhere at the same time.

EndpointScopeToken owner's role
GET /usageusage:readAdmin
GET /usage/exportusage:readAdmin

Both routes need an owner who is currently an organization Admin — the same rule as the portal's Analytics page. A Member's token gets 403 insufficient_scope.

Two rules make this data safe to build on:

  • Breakdowns sum to totals — usually. For every field of totals except units, the sum over breakdown[] equals the total exactly when reconciled is true (the normal case). totals are never raised to match the breakdown; usage that cannot be attributed to a group, plus the tail beyond the top 20 groups, is reported in an other row rather than dropped. In rare cases the breakdown can briefly exceed the totals; the response then has reconciled: false and a breakdown_note. Retry for a consistent report — see below.
  • No content, ever. Usage reports counts, money and identifiers only. Prompt and completion content, error text and client addresses are never returned by this API.

Money is reported twice: cost_nanousd is the exact integer amount in nano-USD (10⁻⁹ USD), and cost_usd is the same amount as a decimal string ("12.345678901"). Sum cost_nanousd or parse cost_usd as a decimal — never as a float.

Query usage

GET /api/v1/mgmt/usage

Aggregated usage and cost over [from, to), broken down by one dimension.

Query parameterTypeRequiredNotes
fromstringnoRFC 3339, inclusive. Default: to minus 30 days.
tostringnoRFC 3339, exclusive. Default: now. The range may span at most 366 days.
group_bystringnoOne of project, model, key, day. Default model. One dimension per call.
project_idstringnoOnly usage of this project.
key_idstringnoOnly usage of this API key.
modelstringnoOnly usage of this model (provider/model).

GET /usage is a report, not a list: it is not paginated, and limit or cursor return 422 unsupported_filter, as does any other unknown parameter.

group_bybreakdown[].keyNotes
modelThe model (provider/model)
projectThe project id
keyThe API key id
dayThe UTC date (2026-09-23)One exact row per UTC calendar day; the first and last may be partial days. At most 31 days per call.

For project, model and key, the top 20 groups are returned individually; the rest, plus any usage that could not be attributed (for example requests with no key), are folded into a single row with key: "other".

The usage report

FieldTypeNotes
from, tostringThe window actually read.
group_bystringThe dimension of breakdown.
currencystringAlways USD.
totalsobjectThe usage quantities for the whole window (below), plus units.
breakdownobject[]One row per group: key plus the usage quantities. Sums to totals exactly, field by field, when reconciled is true.
reconciledbooleantrue when breakdown sums to totals exactly (always the case for group_by=day). false in the rare case the breakdown briefly exceeds totals; breakdown_note then explains. Retry for a consistent report.
breakdown_notestringPresent only when reconciled is false.
usage_stalebooleantrue when usage counts may be incomplete for the most recent minutes. Money is always exact.
usage_as_ofstring | nullUsage counts are complete up to this instant (null when unknown).

Every total and every breakdown row carries the same usage quantities:

FieldNotes
requestsMetered requests.
errorsRequests that ended in an error status.
input_tokens, output_tokens, total_tokensToken counts.
cached_input_tokensPrompt-cache read tokens (a subset of input_tokens).
cache_write_tokensPrompt-cache write tokens (a subset of input_tokens).
reasoning_tokensReasoning tokens (a subset of output_tokens).
cost_nanousdBilled cost, integer nano-USD.
cost_usdThe same cost as a decimal string.

totals.units[] gives the native-unit quantity per billing unit (token, image, second, character, document, …) — unit, requests, quantity_in, quantity_out, cost_nanousd, cost_usd. Quantities of different units are never summed together.

curl -G https://portal.<domain>/api/v1/mgmt/usage \
  -H "Authorization: Bearer $TF_PAT" \
  -d from=2026-09-01T00:00:00Z \
  -d to=2026-09-08T00:00:00Z \
  -d group_by=model
{
  "from": "2026-09-01T00:00:00Z",
  "to": "2026-09-08T00:00:00Z",
  "group_by": "model",
  "currency": "USD",
  "totals": {
    "requests": 105,
    "errors": 2,
    "input_tokens": 1500,
    "output_tokens": 700,
    "total_tokens": 2200,
    "cached_input_tokens": 40,
    "cache_write_tokens": 0,
    "reasoning_tokens": 0,
    "cost_nanousd": 3123456789,
    "cost_usd": "3.123456789",
    "units": [
      { "unit": "token", "requests": 105, "quantity_in": 1500, "quantity_out": 700, "cost_nanousd": 3123456789, "cost_usd": "3.123456789" }
    ]
  },
  "breakdown": [
    {
      "key": "openai/gpt-4o",
      "requests": 60,
      "errors": 0,
      "input_tokens": 1000,
      "output_tokens": 500,
      "total_tokens": 1500,
      "cached_input_tokens": 0,
      "cache_write_tokens": 0,
      "reasoning_tokens": 0,
      "cost_nanousd": 2000000000,
      "cost_usd": "2.00"
    },
    {
      "key": "other",
      "requests": 45,
      "errors": 2,
      "input_tokens": 500,
      "output_tokens": 200,
      "total_tokens": 700,
      "cached_input_tokens": 40,
      "cache_write_tokens": 0,
      "reasoning_tokens": 0,
      "cost_nanousd": 1123456789,
      "cost_usd": "1.123456789"
    }
  ],
  "reconciled": true,
  "usage_stale": false,
  "usage_as_of": "2026-09-24T09:59:15Z"
}

Need two dimensions — per project per day, say? Make one group_by=day call per project (project_id=…), or one group_by=project call per day. Several values in one call (group_by=project,day) are refused with 422 unsupported_filter.

Export usage

GET /api/v1/mgmt/usage/export

The backfill path: one record per metered request in [from, to), newest first, streamed as JSON Lines or CSV.

Query parameterTypeRequiredNotes
formatstringnojsonl (default; application/x-ndjson, one record per line) or csv (with a header row).
from, tostringnoAs for GET /usage: RFC 3339, default the last 30 days, at most 366 days.
project_id, key_id, modelstringnoAs for GET /usage.

There is no group_by, and the export is not paged: limit, cursor and any other parameter return 422 unsupported_filter.

At most 10,000 records per call. A window that matches more is refused with 422 export_too_large before anything streams — narrow from/to and export in windows, or read the aggregates with GET /usage. Requests running longer than 60 seconds are cut off.

Each record:

FieldNotes
idThe usage record id.
created_atWhen the request was metered.
project_id, key_id
model, provider
statusThe request's outcome, e.g. ok or error.
unitThe billing unit (token, image, …).
input_tokens, output_tokens, total_tokens, cached_input_tokens, cache_write_tokens, reasoning_tokensToken counts.
quantity_in, quantity_outNative quantity for non-token units (images, seconds, …).
cost_nanousd, cost_usdThe request's cost.
cost_availablefalse when this record's cost could not be read — the cost fields are then 0, not a real zero.
latency_ms
streamingWhether the request streamed.

Truncation

An export can stop early after the 200 status has been sent. A truncated export is therefore not an error response; it is a stream that ends with a marker line:

  • JSONL: a last line {"truncated":true,"rows":N,"reason":"row_cap"|"upstream_error"}.
  • CSV: a last line # truncated after N rows (<reason>).

A response that was cut short also carries an X-Export-Truncated: true HTTP trailer (declared in the Trailer header) — but trailers are sent after the body and some proxies silently drop them, so treat the trailer as a convenience, not the source of truth. The final line of the stream is the reliable signal: always check for the truncation marker before treating an export as complete.

reason: row_cap means the window matched more than 10,000 records after all — narrow from/to and export the rest separately. reason: upstream_error means reading the usage data failed mid-stream — re-run the same export.

The export reads records newest first, in pages of 200, with a cursor on (created_at, id). Records that share the same created_at are all exported, each exactly once, however many there are.

Every completed export is recorded in the audit log as usage_export, with row_count, format, from and to — bulk data movement deserves the same trail as any other.

curl -G https://portal.<domain>/api/v1/mgmt/usage/export \
  -H "Authorization: Bearer $TF_PAT" \
  -d format=csv \
  -d from=2026-09-23T00:00:00Z \
  -d to=2026-09-24T00:00:00Z \
  -o usage-2026-09-23.csv
{"id":"7c2e4f1a-9b3d-4e8a-a1c6-5d0f2b8e3a97","created_at":"2026-09-23T23:59:41.203Z","project_id":"5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08","key_id":"c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74","model":"openai/gpt-4o","provider":"openai","status":"ok","unit":"token","input_tokens":812,"output_tokens":164,"total_tokens":976,"cached_input_tokens":0,"cache_write_tokens":0,"reasoning_tokens":0,"quantity_in":0,"quantity_out":0,"cost_nanousd":3670000,"cost_usd":"0.00367","cost_available":true,"latency_ms":1840,"streaming":true}

Errors specific to usage

StatusCodeWhen
400invalid_request (param set)from/to not RFC 3339; from not before to; a range over 366 days; group_by not one of the four values; project_id/key_id not an id; format not jsonl/csv.
403insufficient_scopeToken lacks usage:read, or the owner is not currently an Admin.
422unsupported_filterAn unknown parameter, limit/cursor, or several group_by values.
422validation_failedgroup_by=day over a range longer than 31 days.
422export_too_largeThe export window matches more than 10,000 requests.
502upstream_unavailableUsage data is temporarily unavailable. Retry.

To reconcile usage with balances and invoices, see Billing.