Skip to content
INFRO

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. A from earlier than the kind's retention window — 90 days for requests, 12 months for audit — is clamped to it; usage reaches 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"]
    }
  }'
Response
{
  "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 queuedrunningsucceeded 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

Completed job
{
  "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.

ColumnDescription
request_idThe gateway's id for the request. Fetch the full record — spans, fallback attempts — with GET /v1/requests/{id}.
timestampWhen the gateway received the request.
model_requestedThe model id the request asked for — differs from model when a fallback served it.
modelThe model that actually served, after any fallbacksvendor/model-name form, e.g. anthropic/claude-sonnet-5.
routeThe provider that served the request — the same slug the response's provider field reported.
http_statusHTTP status returned to your client, e.g. 200, 402.
latency_msTotal gateway latency, request received to response complete.
first_token_msTime to first token on streamed requests; empty otherwise.
tokens_inPrompt tokens. Empty for non-token modalities.
tokens_outCompletion tokens. Empty for non-token modalities.
finish_reasonWhy generation stopped — stop, length, or tool_calls — on text requests; empty for media.
unit_qtyBillable quantity: total tokens for text, seconds for video and audio, image count for image models.
unitWhat unit_qty is denominated in: tokens, images, seconds, or characters.
costExact USD charged — the same figure as the response's usage.cost.
cost_at_directWhat 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_idmetadata.user_id, if the request carried one.
session_idmetadata.session_id, if present.
tagsThe 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.

ColumnDescription
dayUTC calendar day, e.g. 2026-07-14.
modelModel id, e.g. bfl/flux-2-pro.
projectProject id (proj_...) — the same identifier trace records and analytics report.
keyKey label — see Authentication.
requestsRequest count in the bucket.
unitsBillable units served (tokens, images, seconds, characters — per the model's unit).
costUSD charged.
cost_at_directUSD the same traffic would have cost at the providers' direct prices.
error_rateFraction of requests (0–1) that returned a 4xx or 5xx.
p50_msMedian total latency in the bucket.
p95_ms95th-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.

ColumnDescription
idEvent id.
eventEvent type, e.g. key.created, member.role_changed. The full list is on the audit log page.
actorThe member (or system process) that performed the action.
targetWhat was acted on — a key id, member id, or project id.
timestampWhen it happened, UTC.
detailEvent-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:

SpanParentCovers
gateway— (root)The whole request inside INFRO: auth, validation, response write. Its duration is the request's latency_ms.
route_selectiongatewayScoring providers and choosing a route — see Smart routing.
providergatewayThe 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.
streamgatewayResponse 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: false shows up in exports and traces with full metadata; only its content was never stored.
  • Request records last 90 days. GET /v1/requests and requests exports read the same store, so a from earlier 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 usage export's from can reach back to your first request — see Privacy.
  • Audit events last 12 months. An audit export with an earlier from is 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 requests export.

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.