Documentation
Usage analytics
Aggregate INFRO usage with GET /v1/usage: spend, units, error rate, and latency grouped by model, project, key, end user, or tag, per hour or per day.
GET /v1/usage returns your traffic pre-aggregated into time buckets: requests, billable units, exact cost, what the same traffic would have cost at the direct price, error rate, and latency percentiles. Group buckets by model, project, key, end user, or any metadata tag, and most operational questions — what did each model cost this month, which customer is expensive to serve, where did p95 go — get answered without re-summing raw logs.
The console dashboards read this same API, so anything a dashboard shows you can fetch and feed into your own tooling. For individual request records use request logs; to move bulk history into a warehouse use exports; to be told when a number crosses a threshold instead of polling, set an alert.
Querying usage
One GET, shaped entirely by query parameters. Timestamps are ISO 8601 UTC; from is inclusive and to is exclusive. Omit both and the range defaults to the current billing month — the same window spend limits reset on. Buckets are near-real-time: a completed request is reflected within about a minute, and the current hour or day updates as traffic lands.
curl -G https://api.infro.io/v1/usage \
-H "Authorization: Bearer $INFRO_API_KEY" \
-d granularity=day \
-d from=2026-08-17T00:00:00Z \
-d to=2026-08-24T00:00:00Z{
"group_by": null,
"granularity": "day",
"from": "2026-08-17T00:00:00Z",
"to": "2026-08-24T00:00:00Z",
"data": [
{
"bucket": "2026-08-17T00:00:00Z",
"group": null,
"requests": 48210,
"units": 21384520,
"cost": 96.14,
"cost_at_direct": 131.02,
"error_rate": 0.0018,
"p50_ms": 640,
"p95_ms": 2210
},
{
"bucket": "2026-08-18T00:00:00Z",
"group": null,
"requests": 51877,
"units": 23930116,
"cost": 104.63,
"cost_at_direct": 142.55,
"error_rate": 0.0034,
"p50_ms": 655,
"p95_ms": 2380
}
],
"has_more": false,
"next_cursor": null
}group_bystringmodel,project,key,user_id, ortag.<key>(for exampletag.feature). One bucket per group per time window. Omit for a single org-wide series —groupis thennullin every bucket.granularitystringhourorday(default). Buckets align to UTC clock boundaries regardless offrom.fromstring- Inclusive range start, ISO 8601 UTC. Defaults to the start of the current billing month.
tostring- Exclusive range end, ISO 8601 UTC. Defaults to now.
cursorstring- Opaque pagination cursor from a previous response's
next_cursor. limitinteger- Buckets per page. Default 25, max 100.
Bucket fields
Every bucket carries the same seven measures, whatever the grouping:
| Field | Type | Description |
|---|---|---|
requests | integer | Requests completed in the bucket, errors included. |
units | integer | Billable units consumed: tokens for text models, images for image models, seconds for video and audio. |
cost | number | Exact USD charged — the sum of usage.cost over every request in the bucket. |
cost_at_direct | number | USD the same traffic would have cost at the direct price — each request re-priced at its serving provider's published rate. |
error_rate | number | Fraction of requests (0 to 1) that ended in a 4xx or 5xx status. |
p50_ms | integer | Median end-to-end latency in milliseconds, successful requests only. |
p95_ms | integer | 95th-percentile end-to-end latency in milliseconds, successful requests only. |
cost is the dollars you were actually charged, including any prompt caching discounts. cost_at_direct is what the identical traffic would have cost had you called each provider directly at its list rate. The difference between the two is your savings — the number the console's savings widget shows is exactly cost_at_direct minus cost from this endpoint.
cost_at_direct is computed per request at serving time, against the provider's direct price in force at that moment. Historical buckets don't shift when a provider changes its prices later.
unitsare only comparable within one model. Tokens, images, and seconds are different things — in a group that spans modalities (group_by=project, say, coveringopenai/gpt-5.1andbfl/flux-2-protraffic) treatunitsas opaque volume and comparecostinstead.- Latency is end to end — request received to last byte sent. For streams that spans the whole stream, so a long generation is not a slow gateway; first-token latency is recorded per request in request logs.
- With BYOK,
costis what INFRO charged (the 5% fee), not what the provider billed you directly.
Grouping dimensions
group_by | One bucket per | Group value |
|---|---|---|
model | Model that served the request | Model ID, e.g. anthropic/claude-sonnet-5 |
project | Project the requesting key belongs to | Project ID, as in /v1/projects/{id} |
key | API key | The key's label, e.g. prod-checkout — set in the console; see Authentication |
user_id | End user of your product | The metadata.user_id string you sent |
tag.<key> | Distinct value of one tag, e.g. group_by=tag.feature | The tag's value, e.g. summarize |
user_id and tag.<key> only exist if you send them. metadata is a top-level request field accepted on every endpoint — a user_id, a session_id, and up to 16 string tags with values up to 256 characters, all indexed for filtering and grouping. The full schema lives in request logs.
{
"model": "openai/gpt-5.1",
"messages": [{ "role": "user", "content": "..." }],
"metadata": {
"user_id": "u_3921",
"session_id": "sess_a41f",
"tags": { "feature": "summarize", "plan": "pro" }
}
}Requests missing the dimension aggregate under a null group — a large null group under group_by=user_id means part of your fleet isn't sending metadata yet. When fallbacks trigger, the request counts under the model that actually served it, matching the response's model field.
Worked examples
Spend per model this month
With from and to omitted, the range is the billing month to date. Sum cost across each group's daily buckets — and sum cost_at_direct alongside it if you want the savings number too.
curl -G https://api.infro.io/v1/usage \
-H "Authorization: Bearer $INFRO_API_KEY" \
-d group_by=modelCost per end user
If your requests carry metadata.user_id, this is per-customer cost of goods — the basis for metering, margin analysis, or finding the one user behind a spend spike.
curl -G https://api.infro.io/v1/usage \
-H "Authorization: Bearer $INFRO_API_KEY" \
-d group_by=user_id \
-d from=2026-08-01T00:00:00Z \
-d to=2026-09-01T00:00:00ZEach group is one user_id; sum cost across its buckets. To see what an expensive user actually did, drill into their raw rows with GET /v1/requests?user_id=u_3921 — see request logs.
p95 latency per model
Hourly p95 per model over the last day. Latency regressions usually show up here first:
curl -G https://api.infro.io/v1/usage \
-H "Authorization: Bearer $INFRO_API_KEY" \
-d group_by=model \
-d granularity=hour \
-d from=2026-08-23T00:00:00Z \
-d to=2026-08-24T00:00:00ZWhen a model's p95 shifts, open its request records and check whether the provider mix or the fallback rate changed — routing adapts to provider health, and the records show who served what. For a standing check instead of a manual one, create a p95_latency alert.
Pagination and range limits
Buckets return in ascending time order, groups ordered by cost descending within each window. Pages carry limit buckets (default 25, max 100) plus the standard list envelope: has_more, and a next_cursor to pass back as cursor — null on the last page. High-cardinality groupings — user_id across thousands of users, a busy month at hourly granularity — produce many buckets, so set limit=100 and loop, as in the worked example above.
| Granularity | Max range per query | Retention |
|---|---|---|
hour | 31 days | 90 days |
day | 366 days | Indefinite |
A query outside these bounds — range too long, from not before to, a group_by that isn't a dimension — fails with 400 and type invalid_request_error; see Errors. For history beyond the hourly retention window, or for raw rows in your own warehouse, schedule an export.
Where this fits
- Raw rows — every aggregate here is a roll-up of request logs: per-request routes, fallback attempts, spans, and exact cost.
- Warehouse — exports deliver request and usage history as CSV jobs, and OTLP streaming feeds your own tracing stack.
- Alerting — alerts watch
spend,error_rate, andp95_latencyserver-side, so you don't have to poll this endpoint. - Enforcement — analytics observe; spend controls act, capping spend at the org, project, member, or key level.