Documentation
Request traces
Every INFRO request leaves a trace record — route, fallback attempts, latency, spans, and exact cost — queryable via GET /v1/requests and enrichable with metadata.
Every request through INFRO produces a trace record: the model and provider that served it, how routing chose them, every fallback attempt along the way, latency down to the first token, and the exact USD charged next to what the same call would have cost at the direct price. Records exist for every endpoint — chat, images, video, audio, and async jobs — whether the request succeeded or failed.
This page covers the record itself, the metadata extension that ties records to your own users and sessions, and the two endpoints that read them back. For aggregates over these records, see Analytics; to move raw records into your own stack, see Exports.
What INFRO keeps per request
A record is written for every API call, successful or not, and becomes queryable within a few seconds. Every response also carries an X-INFRO-Request-Id header whose value is the record's id — log it next to your own request IDs and you (or INFRO support) can pull up exactly what happened to any call.
idstringreq_-prefixed record ID. Returned on every API response in theX-INFRO-Request-Idheader.createdinteger- Unix timestamp (seconds) at which the gateway accepted the request.
endpointstring- The API path called, such as
/v1/chat/completions. projectstring- ID of the project the API key belongs to.
keystring- Label of the API key that made the request — the same label shown in the console and in
GET /v1/key(Rate & spend limits). statusstringokfor a 2xx response,errorfor anything else. The exact status code is inhttp_status.errorobject | null- For failed requests, the standard error envelope —
message,type,code— exactly as returned to the caller. See Errors.nullon success. model_requestedstring- The model ID you sent in the request body.
modelstring- The model that actually served the request. Differs from
model_requestedwhen a fallback ran. providerstring- Slug of the provider that served the request, as chosen by routing — the same value the response's
providerfield reported. regionstring- Datacenter region that served the request:
us,eu, orap. routing_policystring- The routing policy in effect:
cheapest,fastest, orbalanced. fallback_attemptsarray- One entry per attempt that failed before the served one, in order. Each carries
model,provider,error(the error type), andlatency_ms. Empty when the first attempt succeeded. latency_msinteger- Total gateway latency in milliseconds, from accepting the request to the last byte out.
first_token_msinteger | null- Milliseconds until the first content token left the gateway (Streaming).
nullfor non-streamed responses. spansarray- Timing breakdown of where the latency went — see the spans section below.
usageobject- Token counts (
prompt_tokens,completion_tokens,total_tokens) pluscost— the exact USD charged — andcost_at_direct, what the same call would have cost at the provider's direct price. Media endpoints report unit counts (images, seconds of audio or video) instead of tokens. metadataobject- Whatever you sent in the request's
metadatafield, verbatim — see below. contentobject | null- The request and response payloads —
messagesin,choicesout, or the media equivalent. Present only while content logging is on and within the content retention window; see Privacy.
The metadata extension
Every endpoint accepts an optional top-level metadata object. INFRO stores it on the trace record verbatim and indexes it, so you can filter the request log by your own end-user or session and group analytics by any tag. The gateway never interprets metadata — it changes nothing about routing, models, or billing.
metadataobject- Top-level request field, accepted on every endpoint. All three members are optional.
metadata.user_idstring- Your identifier for the end user behind the request. Filterable via
user_idonGET /v1/requestsand groupable inGET /v1/usage. metadata.session_idstring- Your identifier for a session or conversation, so a multi-turn interaction can be pulled up as one thread.
metadata.tagsobject- Up to 16 free-form pairs. Keys and values are strings; values up to 256 characters. Each tag is filterable as
tag.<key>and groupable in analytics.
A metadata object that breaks these rules — more than 16 tags, non-string values, an oversized value — fails the whole request with 400 invalid_request_error before it reaches a model. The OpenAI SDKs type metadata as a flat string map, so pass INFRO's richer shape via extra_body in Python and as an untyped extra field in TypeScript, the same way as routing:
curl https://api.infro.io/v1/chat/completions \
-H "Authorization: Bearer $INFRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-sonnet-5",
"messages": [{"role": "user", "content": "Summarize this contract: ..."}],
"metadata": {
"user_id": "usr_8842",
"session_id": "sess_a91k3",
"tags": {"feature": "summarize", "plan": "pro"}
}
}'Metadata is operational metadata, not content: it survives logging: false, appears in analytics and exports, and is visible to your whole org. Send opaque IDs — never emails, names, or anything you would not put in a log line.
List requests
GET /v1/requests returns trace records newest-first, filtered by any combination of the parameters below. Filters combine with AND — user_id plus tag.feature returns only requests matching both. List entries are the full record minus spans and content; fetch a single record for those.
modelstring- Filter to one model ID, such as
anthropic/claude-sonnet-5. Matches the model that served, not the model requested. projectstring- Filter to one project ID.
keystring- Filter to one API key by its label.
user_idstring- Filter to records whose
metadata.user_idmatches exactly. session_idstring- Filter to records whose
metadata.session_idmatches exactly. tag.<key>string- Filter on a metadata tag, e.g.
tag.feature=summarize. Repeat with different keys to require several tags at once. statusstringokorerror.fromstring- Window start, inclusive. RFC 3339 UTC timestamp like
2026-08-01T00:00:00Z. Defaults to 24 hours ago. tostring- Window end, exclusive. Same format. Defaults to now.
cursorstring- Opaque pagination cursor from a previous response's
next_cursor. limitinteger- Records per page, 1–100. Default 25.
curl "https://api.infro.io/v1/requests?user_id=usr_8842&status=ok&limit=2" \
-H "Authorization: Bearer $INFRO_API_KEY"{
"object": "list",
"data": [
{
"id": "req_7f3k9q2v8n01",
"created": 1787481600,
"endpoint": "/v1/chat/completions",
"project": "proj_4kq8b2",
"key": "prod-checkout",
"status": "ok",
"http_status": 200,
"error": null,
"model_requested": "openai/gpt-5.1",
"model": "anthropic/claude-sonnet-5",
"provider": "anthropic",
"region": "us",
"routing_policy": "cheapest",
"fallback_attempts": [
{
"model": "openai/gpt-5.1",
"provider": "openai",
"error": "upstream_error",
"latency_ms": 774
}
],
"latency_ms": 2645,
"first_token_ms": 1204,
"usage": {
"prompt_tokens": 1315,
"completion_tokens": 208,
"total_tokens": 1523,
"cost": 0.007065,
"cost_at_direct": 0.007959
},
"metadata": {
"user_id": "usr_8842",
"session_id": "sess_a91k3",
"tags": { "feature": "summarize", "plan": "pro" }
}
},
{
"id": "req_5t8mw1zq4c22",
"created": 1787481374,
"endpoint": "/v1/images/generations",
"project": "proj_4kq8b2",
"key": "prod-checkout",
"status": "ok",
"http_status": 200,
"error": null,
"model_requested": "bfl/flux-2-pro",
"model": "bfl/flux-2-pro",
"provider": "bfl",
"region": "us",
"routing_policy": "cheapest",
"fallback_attempts": [],
"latency_ms": 6412,
"first_token_ms": null,
"usage": {
"units": 1,
"cost": 0.04,
"cost_at_direct": 0.05
},
"metadata": {
"user_id": "usr_8842",
"session_id": "sess_a91k3",
"tags": { "feature": "cover-image", "plan": "pro" }
}
}
],
"has_more": true,
"next_cursor": "cur_qz8xkm41"
}When has_more is true, request the next page by passing next_cursor as cursor with the same filters — cursors are opaque and tied to the filter set, so changing a filter mid-pagination restarts the listing. has_more: false means the window is exhausted.
Fetch one request
GET /v1/requests/{id} returns the full record, including spans and — when content logging was on — the content payloads. IDs come from the list endpoint or from the X-INFRO-Request-Id response header. An unknown ID, or one older than the retention window, returns 404.
curl https://api.infro.io/v1/requests/req_7f3k9q2v8n01 \
-H "Authorization: Bearer $INFRO_API_KEY"{
"id": "req_7f3k9q2v8n01",
"created": 1787481600,
"endpoint": "/v1/chat/completions",
"project": "proj_4kq8b2",
"key": "prod-checkout",
"status": "ok",
"http_status": 200,
"error": null,
"model_requested": "openai/gpt-5.1",
"model": "anthropic/claude-sonnet-5",
"provider": "anthropic",
"region": "us",
"routing_policy": "cheapest",
"fallback_attempts": [
{
"model": "openai/gpt-5.1",
"provider": "openai",
"error": "upstream_error",
"latency_ms": 774
}
],
"latency_ms": 2645,
"first_token_ms": 1204,
"spans": [
{ "name": "gateway", "start_ms": 0, "duration_ms": 2645 },
{ "name": "route_selection", "start_ms": 3, "duration_ms": 9 },
{ "name": "provider", "start_ms": 792, "duration_ms": 1846 },
{ "name": "stream", "start_ms": 1204, "duration_ms": 1434 }
],
"usage": {
"prompt_tokens": 1315,
"completion_tokens": 208,
"total_tokens": 1523,
"cost": 0.007065,
"cost_at_direct": 0.007959
},
"metadata": {
"user_id": "usr_8842",
"session_id": "sess_a91k3",
"tags": { "feature": "summarize", "plan": "pro" }
},
"content": {
"messages": [
{ "role": "user", "content": "Summarize this contract: ..." }
],
"choices": [
{
"message": { "role": "assistant", "content": "This agreement covers..." },
"finish_reason": "stop"
}
]
}
}Reading this record: the request asked for openai/gpt-5.1, every provider for it errored, and the fallback chain moved to anthropic/claude-sonnet-5, served by Anthropic from a US datacenter. The failed attempt cost 774 ms, which is why first_token_ms is 1204 rather than the roughly 410 ms the serving provider actually took to start streaming. cost is the exact USD charged; cost_at_direct is what the served tokens would have cost at the provider's direct price.
Spans
spans breaks the total latency into where the time actually went. Each span has a name, a start_ms offset from created, and a duration_ms.
| Span | Covers |
|---|---|
gateway | The whole request inside INFRO — auth, parsing, policy checks, and everything below. Its duration equals latency_ms. |
route_selection | Scoring healthy providers and picking a route — see Routing. Re-runs after a failed attempt; the span covers the total. |
provider | The served attempt against the upstream provider, from connection to final byte. Failed attempts are summarized in fallback_attempts instead of getting spans. |
stream | First content token to last, for streamed responses. Absent when the response was not streamed. |
The same span tree can be streamed to your own observability stack over OTLP/HTTP — one tree per request, service name infro-gateway, configured in the console. That setup lives in Exports.
Zero-logging and traces
logging: false — per request, or org-wide via the console toggle — controls content, not tracing. A zero-logged request still produces a complete trace record: route, fallback attempts, status, latency, spans, token counts, cost, and metadata are all present, because none of them contain your prompts. What changes is content: it is never written, so it never appears on the record, in exports, or to INFRO support. Metering and billing are unaffected.
The split is deliberate — you keep full operational visibility over workloads whose content must never touch a disk. The exact guarantees, including default content retention, are on Privacy.
Retention
| Data | Window |
|---|---|
| Trace records (route, status, latency, spans, usage, metadata) | 90 days, queryable via GET /v1/requests |
Aggregates (GET /v1/usage) | Hourly buckets 90 days, daily buckets for the life of the account — see Analytics |
content payloads | Up to 30 days with logging on; never written with logging: false — see Privacy |
Records older than 90 days drop out of GET /v1/requests, but their aggregates stay in Analytics and billing totals are never lost. If you need raw records for longer — audits, customer billing, tuning your own routing — schedule recurring CSV exports or stream spans to your own collector over OTLP, where retention is yours to decide. And to act on this data in real time instead of querying it after the fact, set thresholds on spend, error rate, or p95 latency with Alerts.