Documentation
Exports & OpenTelemetry
Get your data out of INFRO: async CSV exports of requests, usage, and audit events, plus OTLP/HTTP trace streaming from the gateway to your collector.
INFRO keeps two kinds of records about your traffic: per-request logs and the aggregates built from them. Both leave the platform whenever you want them to — as batch CSV files through POST /v1/exports, or as a live stream of OpenTelemetry spans to a collector you run. Exports suit finance, capacity reviews, and compliance evidence; OTLP puts gateway spans next to your application traces in whatever backend you already operate.
An export is a standard async job: submit it, poll or take a webhook when it finishes, and download a signed CSV URL from the completed job. Neither exports nor spans ever contain prompt or completion content — they carry the same metadata you see in request logs and usage analytics.
Create an export
POST /v1/exports takes a kind and a time range. Compiling a month of high-volume traffic takes longer than an HTTP request should stay open, so the call returns a job immediately rather than the file.
kindstringrequired"requests"(one row per request),"usage"(daily aggregates), or"audit"(organization audit events). Column sets for each are below.fromstringrequired- Start of the range as an RFC 3339 UTC timestamp, e.g.
"2026-07-01T00:00:00Z". Inclusive. Afromearlier than the kind's retention window — 90 days forrequests, 12 months foraudit— is clamped to it;usagereaches back to your first request. tostring- End of the range, RFC 3339 UTC, exclusive. Defaults to the time of the call.
webhookobject{"url": "https://example.com/hooks/infro", "events": ["job.succeeded", "job.failed"]}. Overrides the account webhook endpoint for this job only — see Webhooks.
curl https://api.infro.io/v1/exports \
-H "Authorization: Bearer $INFRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "requests",
"from": "2026-07-01T00:00:00Z",
"to": "2026-08-01T00:00:00Z",
"webhook": {
"url": "https://example.com/hooks/infro",
"events": ["job.succeeded", "job.failed"]
}
}'{
"id": "job_c41e77a2",
"object": "job",
"type": "export.requests",
"status": "queued",
"model": null,
"provider": null,
"created_at": 1787532000,
"output": null,
"usage": null,
"error": null
}This is the same envelope every asynchronous endpoint returns: status moves queued → running → succeeded or failed, and every field is documented in Async jobs. Poll GET /v1/jobs/{id} from scripts, or configure a webhook and do nothing until a signed job.succeeded delivery arrives — signature verification is covered in Webhooks.
Any API key in the organization can create requests and usage exports. The audit kind is restricted to keys belonging to owners and admins, matching who may read GET /v1/org/audit. Analysts hold no API keys but can run requests and usage exports from the console — that read-only path is what the role is for; audit exports stay owner- and admin-only there too. Creating an export is free, counts against normal rate limits, and is itself recorded in the audit log as export.created.
Retrieving the file
{
"id": "job_c41e77a2",
"object": "job",
"type": "export.requests",
"status": "succeeded",
"model": null,
"provider": null,
"created_at": 1787532000,
"started_at": 1787532002,
"completed_at": 1787532041,
"expires_at": 1787618441,
"output": {
"url": "https://cdn.infro.io/exports/c41e77a2.csv",
"rows": 184203,
"bytes": 41680217,
"mime_type": "text/csv"
},
"usage": null,
"error": null
}output.url is a signed link — no Authorization header needed, safe to hand to a download script or a BI tool's fetch step. It expires at expires_at, 24 hours after completion, and the file is then deleted. output.rows and output.bytes let you sanity-check the download; while the job runs, its progress field reports how far along compilation is.
Download inside the webhook handler or right after the poll succeeds, and copy the file to your own storage. An expired URL is not data loss while the underlying records are still within retention — create a new export for a fresh link — but records that have aged out (90 days for requests, 12 months for audit) cannot be re-exported, so the copy you keep is the history.
CSV columns
Every file is UTF-8 with a header row and RFC 4180 quoting. Timestamps are RFC 3339 UTC. Columns that do not apply to a row — first_token_ms on a non-streamed request, tokens_in on an image job — are empty, not zero.
kind: requests
One row per request in the range — the same records GET /v1/requests pages through, in bulk. An export always covers the whole organization for the range; filter after download, or use the filtered request log API for narrow slices.
| Column | Description |
|---|---|
request_id | The gateway's id for the request. Fetch the full record — spans, fallback attempts — with GET /v1/requests/{id}. |
timestamp | When the gateway received the request. |
model_requested | The model id the request asked for — differs from model when a fallback served it. |
model | The model that actually served, after any fallbacks — vendor/model-name form, e.g. anthropic/claude-sonnet-5. |
route | The provider that served the request — the same slug the response's provider field reported. |
http_status | HTTP status returned to your client, e.g. 200, 402. |
latency_ms | Total gateway latency, request received to response complete. |
first_token_ms | Time to first token on streamed requests; empty otherwise. |
tokens_in | Prompt tokens. Empty for non-token modalities. |
tokens_out | Completion tokens. Empty for non-token modalities. |
finish_reason | Why generation stopped — stop, length, or tool_calls — on text requests; empty for media. |
unit_qty | Billable quantity: total tokens for text, seconds for video and audio, image count for image models. |
unit | What unit_qty is denominated in: tokens, images, seconds, or characters. |
cost | Exact USD charged — the same figure as the response's usage.cost. |
cost_at_direct | What the request would have cost at the provider's direct price. cost_at_direct − cost, summed over a file, is the number finance asks for. |
user_id | metadata.user_id, if the request carried one. |
session_id | metadata.session_id, if present. |
tags | The request's metadata.tags object, JSON-encoded into a single cell. |
kind: usage
Daily aggregates: one row per UTC day per model per project per key that saw traffic. The grouping is fixed. If you need buckets by user_id or by a tag, query GET /v1/usage with group_by instead, or export requests and aggregate yourself.
| Column | Description |
|---|---|
day | UTC calendar day, e.g. 2026-07-14. |
model | Model id, e.g. bfl/flux-2-pro. |
project | Project id (proj_...) — the same identifier trace records and analytics report. |
key | Key label — see Authentication. |
requests | Request count in the bucket. |
units | Billable units served (tokens, images, seconds, characters — per the model's unit). |
cost | USD charged. |
cost_at_direct | USD the same traffic would have cost at the providers' direct prices. |
error_rate | Fraction of requests (0–1) that returned a 4xx or 5xx. |
p50_ms | Median total latency in the bucket. |
p95_ms | 95th-percentile total latency in the bucket. |
kind: audit
One row per audit log event — the same records GET /v1/org/audit returns, in bulk. Owners and admins only.
| Column | Description |
|---|---|
id | Event id. |
event | Event type, e.g. key.created, member.role_changed. The full list is on the audit log page. |
actor | The member (or system process) that performed the action. |
target | What was acted on — a key id, member id, or project id. |
timestamp | When it happened, UTC. |
detail | Event-specific fields, JSON-encoded — e.g. the old and new role on member.role_changed. |
Streaming traces over OTLP
For continuous telemetry, skip the batch loop entirely: configure a collector under Observability → OpenTelemetry in the console and INFRO streams one span tree per request as it completes. Configuration is two fields — an OTLP/HTTP endpoint URL and optional headers, typically for authentication. There is no public API for this setting.
Spans ship over OTLP/HTTP as binary protobuf, POSTed to <endpoint>/v1/traces (the path is appended unless your URL already ends with it). The resource carries service.name: "infro-gateway", and the tree is exactly the spans[] array on a request record — same names, same shape:
| Span | Parent | Covers |
|---|---|---|
gateway | — (root) | The whole request inside INFRO: auth, validation, response write. Its duration is the request's latency_ms. |
route_selection | gateway | Scoring providers and choosing a route — see Smart routing. |
provider | gateway | The upstream call that served the response. Failed provider or fallback attempts do not get spans of their own — as in the request record, they are summarized in its fallback_attempts field. |
stream | gateway | Response streaming to your client, first token to last byte. |
Span attributes carry the request's metadata, never its content: infro.request_id, infro.model, infro.provider, infro.status, infro.tokens_in, infro.tokens_out, infro.cost, plus infro.user_id, infro.session_id, and infro.tag.<key> when the request carried metadata. Use infro.request_id to jump from a trace in your backend to the full record at GET /v1/requests/{id}.
Pointing it at Datadog or Grafana
The INFRO side is always the same two fields; the exact endpoint and header names come from your vendor's OTLP intake documentation. Two common shapes:
# Datadog ingests OTLP through the Datadog Agent or an OpenTelemetry
# Collector you run. INFRO delivers over the public internet, so expose
# the collector's OTLP/HTTP receiver behind HTTPS and make it check auth.
Endpoint: https://otel-ingest.yourcompany.com:4318
Headers: X-Collector-Auth: <token your collector verifies>Export is asynchronous and adds zero latency to requests: spans are batched and usually arrive within a few seconds of request completion. Delivery is at-least-once with short retries; if your collector stays unreachable, spans are dropped rather than queued indefinitely. Treat traces as observability, not accounting — exact cost lives in the request log and GET /v1/usage.
Retention and what leaves the platform
- No content, ever. Exports and spans carry metadata only — prompts and completions never appear in them, even when content logging is on. Logged content is readable solely through
GET /v1/requests/{id}— see Privacy. - Zero-retention requests still appear. A request sent with
logging: falseshows up in exports and traces with full metadata; only its content was never stored. - Request records last 90 days.
GET /v1/requestsandrequestsexports read the same store, so afromearlier than the window is clamped and older records are gone — a request past retention 404s. Schedule a recurring export (monthly works well) to keep history beyond it — see Request logs & traces. - Usage aggregates are kept while your account is active. Billing depends on them, so a
usageexport'sfromcan reach back to your first request — see Privacy. - Audit events last 12 months. An
auditexport with an earlierfromis clamped to the retained window — see Audit log. - CSV URLs last 24 hours; the data does not expire with them. Re-export whenever you need a fresh link — within each kind's retention window.
- OTLP is forward-only. Streaming starts when you configure the collector; there is no backfill. For history, run a
requestsexport.
For interactive drill-down without leaving INFRO, the same records power request logs & traces and usage analytics, and thresholds on the aggregates can page you through alerts.