Skip to content
INFRO

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
Response (trimmed to two buckets)
{
  "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_bystring
model, project, key, user_id, or tag.<key> (for example tag.feature). One bucket per group per time window. Omit for a single org-wide series — group is then null in every bucket.
granularitystring
hour or day (default). Buckets align to UTC clock boundaries regardless of from.
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:

FieldTypeDescription
requestsintegerRequests completed in the bucket, errors included.
unitsintegerBillable units consumed: tokens for text models, images for image models, seconds for video and audio.
costnumberExact USD charged — the sum of usage.cost over every request in the bucket.
cost_at_directnumberUSD the same traffic would have cost at the direct price — each request re-priced at its serving provider's published rate.
error_ratenumberFraction of requests (0 to 1) that ended in a 4xx or 5xx status.
p50_msintegerMedian end-to-end latency in milliseconds, successful requests only.
p95_msinteger95th-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.

  • units are only comparable within one model. Tokens, images, and seconds are different things — in a group that spans modalities (group_by=project, say, covering openai/gpt-5.1 and bfl/flux-2-pro traffic) treat units as opaque volume and compare cost instead.
  • 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, cost is what INFRO charged (the 5% fee), not what the provider billed you directly.

Grouping dimensions

group_byOne bucket perGroup value
modelModel that served the requestModel ID, e.g. anthropic/claude-sonnet-5
projectProject the requesting key belongs toProject ID, as in /v1/projects/{id}
keyAPI keyThe key's label, e.g. prod-checkout — set in the console; see Authentication
user_idEnd user of your productThe metadata.user_id string you sent
tag.<key>Distinct value of one tag, e.g. group_by=tag.featureThe 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.

Tagging a request
{
  "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=model

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

August cost per end user
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:00Z

Each 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:

Hourly latency, last 24 hours
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:00Z

When 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 cursornull 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.

GranularityMax range per queryRetention
hour31 days90 days
day366 daysIndefinite

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.
  • Warehouseexports deliver request and usage history as CSV jobs, and OTLP streaming feeds your own tracing stack.
  • Alertingalerts watch spend, error_rate, and p95_latency server-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.