# Request traces

> Inspect INFRO requests, provider connections, fallback attempts, latency and estimated cost using the request API, with history limited by your plan.

Source: https://infro.io/docs/observability/traces

> **Note:** INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See [Early access](https://infro.io/early-access).

The request log records what your applications sent through INFRO: the model, selected provider connection, outcome, timings, usage and estimated provider cost. It is operational evidence; it is not an invoice for model usage.

## List and retrieve requests

```bash
curl "https://api.infro.io/v1/requests?limit=25&status=error" \
  -H "Authorization: Bearer $INFRO_API_KEY"
```

Use `GET /v1/requests/{id}` for a request detail. `limit` sets the page size, from 1 to 100 (default 20). List responses include `data`, `has_more` and `next_cursor`. Pass that cursor as `after` to fetch the next page, keeping the same filters and `sort`. `sort` is `newest` (the default), `slowest` (latency, highest first) or `costliest` (estimated provider cost, highest first; unpriced requests come last, never as $0). Every list also says what it searched: `window_start` (the oldest instant read), `window_end` (your `to`, or null for now), `retention_days`, and `requested_from` when your plan's window moved the `from` you sent.

## Which keys can read history

A gateway key that can call models cannot automatically read every application's history. Each key carries its own history access, set when it is created or edited in the console:

| History access | What `/v1/requests` and `/v1/usage` return |
| --- | --- |
| `scoped` (default) | Requests from the key's project, or only the key's own requests when it has no project, narrowed to the key's environment when it has one. |
| `organization` | Every request in the organization. Only an owner or admin can grant it. |
| `none` | Nothing; the key can call models and poll its own jobs, and history endpoints answer `403` with code `observability_not_permitted`. |

Environment is matched against the environment recorded when each request ran, not the key's current environment. Requests recorded before this was stored have no environment and are visible only to keys without an environment filter. Query filters narrow what a key can read; they never widen it. A request outside a key's scope answers exactly like an ID that does not exist.

Captured prompt and response bodies (`content`) need separate payload access on the key, granted by an owner or admin. Content is never returned after its retention deadline, even if deletion has not run yet, and shortening the retention setting applies to content already captured.

History is measured back from now, whatever `to` you send. An organization without an active or trialing subscription reads no more than the smallest plan's window.

| Filter | Meaning |
| --- | --- |
| `from`, `to` | ISO timestamps; history is limited by the plan. |
| `status` | `ok` or `error`. |
| `model`, `project`, `key` | Model identifier, project ID or gateway key ID. |
| `provider`, `connection`, `route` | Provider kind (`openai`, `aws-bedrock`), connection ID, or route ID or slug; `route=none` selects direct model calls. |
| `endpoint`, `modality` | Narrow to an endpoint or text, image, video or audio. |
| `surface` | Where the request came from: `gateway` (a gateway key), `playground` (the console Playground) or `studio` (INFRO Studio). Every request carries it as `surface`. Recorded from 2026-10-13; earlier rows were marked where their origin is certain and are otherwise `gateway`. |
| `trace_id`, `workflow`, `step`, `feature` | Narrow to one run, one workflow, one step within it, or one product feature. `workflow`, `step` and `feature` take the same shape the request accepts (up to 64 letters, digits and `_ . : -`); any other value is a 400 `invalid_attribution` rather than an empty list. |
| `served_model`, `finish_reason` | The model id your provider's reply named (the row's `served_model`), or why the model stopped: `stop`, `length`, `tool_calls`, `content_filter` or `other`. Recorded from 2026-09-25; older rows have neither. See [Traffic changes](https://infro.io/docs/observability/traffic-changes). |
| `user_id`, `session_id`, `tag.<name>` | Match values you attached in request metadata. |
| `billed` | Legacy name: true selects positive estimated cost; false selects zero. It does not mean INFRO charged for inference. |

Every filter is an exact match on a value, and each is answered from the whole retained window, not from one page. There is no free-text search over request content. A filter naming something that does not exist in your organization (an unknown route slug, another organization's trace ID) returns an empty list, never an unfiltered one.

### Response cache on a request

`GET /v1/requests` and its export accept `cache=hit|miss|bypass|uncacheable|disabled`. Each request object carries `cache`: `null` when the request never reached the cache step, otherwise `{status, source_request_id, reason, provider_billed, estimated_provider_cost_avoided}`. `reason` explains an `uncacheable` request, or a `miss` whose lookup failed (`lookup_error`, `lookup_timeout`). `estimated_provider_cost_avoided` is a USD number on hits only, `null` when the source request was unpriced. See [Response cache](https://infro.io/docs/features/caching).

## Find a request in the console

The search box on **Requests** takes an identifier and searches the whole retained window:

| You type | What happens |
| --- | --- |
| `req_…` | Opens that request. If it is not found, the console says there is no request with that ID in this organization's last N days, which covers a mistyped ID, an ID from another organization and a request older than your plan's window alike. |
| `trace:<id>`, `user:<id>`, `session:<id>` | Filters by `trace_id`, `user_id` or `session_id`. A trace filter also offers the run's trace view. |
| `workflow:<name>`, `step:<name>`, `feature:<name>`, `served_model:<id>` | Filters by that attribution value. |
| `tag.<key>:<value>` or `tag.<key>=<value>` | Filters by a tag you attached in `metadata.tags`. |
| Any other value | The console asks whether to search it as a trace ID, user ID or session ID, rather than guessing. |

Each search becomes a removable filter chip and is written to the page address, so a narrowed view can be shared as a link.

## Export requests as CSV

```bash
curl -o requests.csv "https://api.infro.io/v1/requests/export?trace_id=run_42" \
  -H "Authorization: Bearer $INFRO_API_KEY"
```

`GET /v1/requests/export` takes the same filters as `GET /v1/requests` and returns exactly the rows that list would return, in the same `sort` order (newest first by default), as one CSV file. It needs the same history access as the list, and a scoped key exports only its own scope. The console's **Export CSV** button on Requests downloads the current filters and period.

- **Up to 50,000 requests per file.** A larger result is refused before any data is sent, with `400` and code `export_too_large`: narrow the window or add a filter. A file is never cut short silently. `dry_run=true` returns the row count and the window as JSON without producing a file.
- **Content is never exported.** Prompts, responses and captured payloads are not in the file, even when payload logging is on and the key may read content.
- **`estimated_provider_cost_usd` is an estimate of what your provider will bill you**, not an INFRO charge. It is empty for an unpriced request, never `0`, and `price_source` is empty with it. A request refused before anything ran is a known `0`.
- When you send no `to`, the window ends at the moment the export starts, so requests served while the file streams are not included.
- Every export is recorded in your organization's audit log (who, the filters, the row count) and exports are rate limited per organization.

| Columns, in order |
| --- |
| `id`, `created_at` (ISO 8601, UTC, milliseconds), `endpoint`, `modality`, `status`, `http_status`, `error_code`, `project_id`, `project_name`, `key_id`, `key_name`, `provider`, `connection_id`, `connection_name`, `route_slug`, `model_requested`, `model`, `served_model`, `finish_reason`, `prompt_tokens`, `completion_tokens`, `units`, `estimated_provider_cost_usd`, `price_source`, `latency_ms`, `ttft_ms`, `fallback_attempts` (a count), `trace_id`, `workflow`, `step`, `feature`, `user_id`, `session_id`, `tags` (a JSON object) |

The column contract is versioned by the `X-INFRO-Export-Schema-Version` response header (currently `1`); new columns are only ever added at the end. Headers also carry `X-INFRO-Export-Rows`, `X-INFRO-Window-Start`, `X-INFRO-Window-End` and `X-INFRO-Retention-Days`. Every field is quoted, lines end in CRLF, and the file has no byte-order mark. A text value that would start a spreadsheet formula (`=`, `+`, `-`, `@`, tab, carriage return) is prefixed with `'`; numbers are left as numbers. If Excel shows accented characters wrongly, open the file with Data → From Text/CSV and choose UTF-8.

Two columns are added at the end, after `tags`: `cache_status` and `cache_source_request_id`. The schema version stays `1`, because columns are only ever added at the end.

## Group requests into a run

A retrieval-augmented answer or an agent turn is several model calls. Attach three reserved keys to `metadata` and INFRO records them as indexed columns, so you can ask what one run of a workflow cost rather than what one request cost.

| Key | Meaning | Shape |
| --- | --- | --- |
| `trace_id` | One run. Generate it yourself, or propagate W3C trace context. | Up to 128 characters of letters, digits, `_ . : -` |
| `workflow` | The stable name of the work: `support-answer`, `nightly-indexer`. | Up to 64 characters, same character set |
| `step` | This call's role in the run: `embed_query`, `rerank`, `generate`. | Up to 64 characters, same character set |
| `feature` | The product feature the call serves: `summarize`, `search`. Groups cost by feature and matches cost-center rules. See [Cost attribution](https://infro.io/docs/features/attribution). | Up to 64 characters, same character set |

A malformed reserved key is refused with `400` and code `invalid_attribution` naming the key, rather than being dropped. Attribution that fails quietly is discovered weeks later as a hole in a cost report; a refusal is discovered on the first request of an integration. Every other `metadata` key stays free-form.

Requests that cannot carry a JSON body — multipart audio, for example — can send `X-INFRO-Trace-Id`, `X-INFRO-Workflow`, `X-INFRO-Step`, `X-INFRO-Feature` and `X-INFRO-User-Id` (for `user_id`) instead. A W3C `traceparent` supplies the trace ID when nothing else does, and always nests INFRO's OpenTelemetry spans inside your own trace. Precedence is body metadata, then headers, then `traceparent`.

`GET /v1/traces/{trace_id}` returns every request in one run in order, with cost by step and by modality. The total covers model calls made through INFRO and nothing else: your vector database, your retrieval and your own compute are not included. It is a lower bound whenever any successful call in the run is unpriced, and `cost_is_partial` says so rather than the figure being filled in.

`GET /v1/usage?group_by=workflow` — also `step`, `feature`, `user` and `tag:<key>` for a promoted tag — reports the same figures aggregated. These are the groupings whose cardinality you choose, so they return the hundred largest by estimated cost plus one `__other__` row holding the remainder, with `groups_truncated` set. The rows still add up. Requests that carried no value are one `group: null` row, never folded into `__other__`, so you can see how much traffic is unattributed. Attribution is recorded from the request that first carries it; it is not applied retroactively. `trace_id` is not a grouping: a row per run is what `GET /v1/traces/{trace_id}` and Unit economics are for.

### Tool calls in a trace

Tool calls made through the [MCP gateway](https://infro.io/docs/mcp-gateway) with the same trace id are returned by `GET /v1/traces/{trace_id}` beside the model requests, under the same key scope, history window and cap. The response gains `tool_calls` (in time order: `id`, `created_at`, `server_id`, `server_slug`, `server_name`, `tool`, `outcome`, `error_code`, `latency_ms`, `step`, `workflow`; metadata only), `tool_call_count`, `tool_calls_truncated` and `covers_tool_calls: true`. Every existing field keeps its meaning: `requests`, `totals` and the estimated cost cover model calls only, and `covers` is unchanged. `outcome` is `ok`, `tool_error`, `refused`, `upstream_error`, `timeout` or `cancelled`. A trace that has only tool calls is still found; its start, end and duration then come from the tool calls.

```json
{
  "trace_id": "run-7f3a",
  "requests": [ ... ],
  "tool_calls": [
    {
      "id": "gtc_01JA2Q8R5W",
      "created_at": "2026-10-07T09:14:03.512Z",
      "server_id": "mcps_01JA1V0K7M",
      "server_slug": "github",
      "server_name": "GitHub",
      "tool": "github_list_issues",
      "outcome": "ok",
      "error_code": null,
      "latency_ms": 412,
      "step": "triage",
      "workflow": "issue-triage"
    }
  ],
  "tool_call_count": 1,
  "tool_calls_truncated": false,
  "covers": "model_calls_through_infro",
  "covers_tool_calls": true
}
```

## Read a request record

- Provider and connection details identify your own account that served the request. Fallback attempts record their models, route positions, errors and timings.
- `usage.cost` is an estimated provider cost in USD, with a price source when priced. An unpriced model is not a free model. Your provider invoice is authoritative.
- `cost_breakdown` itemizes the estimate when available. It can be null when a record was not priced or predates itemization.
- Optional `content` is available only when capture was allowed and the payload has not expired. Customer-supplied metadata may itself contain personal data.
- `guardrails` is `{action, findings}` when your organization's [guardrails](https://infro.io/docs/features/guardrails) acted on the request, and null otherwise. `action` is `blocked_input`, `blocked_output`, `redacted` or `flagged`; each finding is `{detector, stage, action, count}`. The matched text is never stored, so the record holds counts only.

## History and export

Developer includes 7 days of queryable history, Startup 14, Team 30, Business 90 and Enterprise 365. The API reports the applied window. Optional payload logging is a separate opt-in with plan limits; see [Privacy](https://infro.io/docs/privacy).

Use the request API or the CSV export above to keep your own history, or configure OpenTelemetry on an eligible plan. There are no scheduled or emailed exports. See [Exports and OpenTelemetry](https://infro.io/docs/observability/exports).
