# FOCUS export

> Export INFRO usage as a FOCUS v1.4 Cost and Usage dataset, with per-project, per-key, per-model and per-tag attribution a provider invoice cannot give you.

Source: https://infro.io/docs/observability/focus-export

> **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 [FinOps Open Cost and Usage Specification](https://focus.finops.org/) (FOCUS) is the FinOps Foundation's interchange format for billing data. `GET /v1/usage/focus` returns your INFRO usage in it, so a FinOps platform, a data warehouse or a spreadsheet can read AI spend beside the rest of your cloud cost — broken down by project, key, environment, model, provider, connection, route and your own metadata tags.

This page is the integration contract. It states which FOCUS columns are populated and from what, which are deliberately absent and why, what is an estimate and what is a reconciled figure, and how the dataset is versioned and paginated.

> **Note:** Written against **FOCUS v1.4**, ratified 4 June 2026. Column definitions quoted on this page were read from the specification on 20 September 2026.

## The one thing to read before integrating

**INFRO is not the invoice issuer for your model usage.** You connect your own provider accounts; OpenAI, Anthropic, AWS or Google invoices you directly, and INFRO never sees that invoice. Everything that follows is downstream of that fact.

FOCUS defines `BilledCost` as the cost of a charge "as invoiced by the invoice issuer", and requires that it "MUST reflect amounts as invoiced by the InvoiceIssuerName, not estimated or inferred values". INFRO's per-request cost is a rate-table calculation made at the moment of the request. It is therefore **never** written to `BilledCost`.

Instead it is written where the specification says a rate calculation goes — `ListCost` and `ContractedCost`, each defined as the product of a unit price and a pricing quantity, neither of which requires an invoice. Both carry the same figure: INFRO's estimate at the rate the request was priced at. `BilledCost` and `EffectiveCost` are populated only from figures your provider's own cost API reported, and are otherwise absent from the dataset rather than estimated or zeroed.

| Column | What INFRO puts in it |
| --- | --- |
| `ListCost` | INFRO's estimate for the charge: quantity x the rate the request was priced at. An estimate of what your provider will bill. Always equal to `ContractedCost`; see Known limits for what that means on a negotiated-rate row. |
| `ContractedCost` | The same estimate. `ContractedUnitPrice` is the rate the request was priced at: your negotiated rate when one was on record, otherwise the vendor's published rate. |
| `BilledCost` | Your provider's own reported figure, from its cost API, allocated to each row in proportion to its share of your organization's estimate for that provider-day. Present only with `cost_basis=reconciled`. |
| `EffectiveCost` | Equal to `BilledCost`. No commitments or amortisation are modelled, and FOCUS requires the two to be equal for uncovered usage charges. |

## Cost basis

- `cost_basis=estimated` (default) — Every provider-day in the window. `ListCost` and `ContractedCost` are populated; `BilledCost` and `EffectiveCost` are **omitted from the dataset**. The response reports `focus.conformance: "partial"` and names the omitted columns with the reason. This is the mode most organizations can use today.
- `cost_basis=reconciled` (requires charge_period=day) — Only provider-days for which your provider reported a cost figure. `BilledCost` and `EffectiveCost` are populated from it, and `focus.conformance` is `"conformant"`. Requires a billing credential for that provider, a Business-plan feature available for OpenAI and Anthropic only — see [Billing](https://infro.io/docs/org/billing).

Reconciled mode requires a daily charge period because that is the grain both supported provider cost APIs report at. Allocating one day's invoiced figure across hours, or summing a month from days that are not all reconciled, would put an estimate back into a column that must not hold one, so it is refused rather than approximated.

> **Warning:** A provider-day containing any unpriced request is **excluded** from a reconciled export, with the reason. The invoice covers that traffic too, so allocating all of it across only the priced rows would inflate every one of them.

## Unpriced usage is reported, never zeroed

All four FOCUS cost metrics are mandatory and may not be null, and FOCUS v1.4 has no representation for "the price of this is not known". For AI workloads that is not an edge case: a model ships, you route to it the same afternoon, and no rate for it exists anywhere yet.

Emitting a zero would silently understate your AI COGS. Dropping the rows would do the same, more quietly. So unpriced usage is returned in a separate `unpriced` collection with **no cost column at all** — it cannot be summed into a cost total, and you can still see which connection, project, key, environment, route and model are involved. Unpriced rows carry ids rather than names, do not break out your `tag_keys`, and report `ConsumedQuantity` as input plus output tokens (`Tokens`) or, for non-token models, `Units`. The collection is returned in full with every page; it is not paginated.

```json
{
  "ChargePeriodStart": "2026-09-19T00:00:00Z",
  "ChargePeriodEnd":   "2026-09-20T00:00:00Z",
  "InvoiceIssuerName": "OpenAI",
  "BillingAccountId":  "conn_4Fh2Kq",
  "SubAccountId":      "proj_search",
  "ConsumedQuantity":  "42000.000000",
  "ConsumedUnit":      "Tokens",
  "x_InfroModelId":    "openai/gpt-5.9-preview",
  "x_InfroRequestCount": "17",
  "x_InfroUnpricedReasonName": "NoRateOnRecord"
}
```

In CSV, unpriced rows are a **separate download** (`unpriced=true`). Mixed into one file they would read as a column of blanks that sum to nothing, which is the same understatement arriving through the file format.

## The grain: a row is a charge

One row is one charge period, one attribution tuple (including workflow and end user, when the request carried them), one priced component, and one rate — not one API request. A FOCUS row is a charge, which is how a cloud provider's dataset is shaped and what a FinOps platform's ingestion expects.

**One request therefore becomes several rows.** Input tokens and output tokens are priced at different rates, so a single row holding both has no single unit price and no single quantity — and FOCUS requires that `ListCost` equal the product of `ListUnitPrice` and `PricingQuantity` whenever both are present. The components are `input`, `cached_input`, `cache_write`, `cache_write_1h`, `output`, `units` and `rounding`, plus `provider_reported` for a request whose cost the provider reported itself (`price_source: provider`, for example OpenRouter): one row carrying that cost, with no SKU price, quantity or unit price, because no rate was applied.

The rate is part of the row key because `SkuPriceId` identifies a thing bought *at a price*: a vendor changing a rate mid-period produces two rows rather than one whose unit price no longer multiplies out.

> **Note:** **The sum is the contract.** Over any window, the sum of `ListCost` equals the sum of the per-request cost INFRO recorded for rate-priced requests, exactly, to the micro-dollar — rounding included, which is why `rounding` is its own component rather than being absorbed. A request whose cost the provider reported itself is included at that cost.

Requests answered from the [response cache](https://infro.io/docs/features/caching) are not in the dataset. Your provider never saw them and bills nothing for them, so there is no charge to report.

## Quantities and units

`PricingQuantity` is measured in `PricingUnit`, which is how the provider rates usage. `ConsumedQuantity` is the raw count. For tokens these are different numbers, and conflating them makes a published unit price a million times wrong.

| INFRO rate unit | PricingUnit | PricingQuantity | ConsumedUnit |
| --- | --- | --- | --- |
| per 1M tokens | `1000000 Tokens` | tokens / 1,000,000 | `Tokens` |
| per image | `Images` | images | `Images` |
| per second | `Seconds` | seconds | `Seconds` |
| per track | `Tracks` | tracks | `Tracks` |
| per 1M characters | `1000000 Characters` | characters / 1,000,000 | `Characters` |
| per audio hour | `Hours` | seconds / 3,600 | `Seconds` |

FOCUS `UnitFormat` requires a unit quantity "expressed as a positive integer" before a capitalised base unit, which is why a per-million-token rate is `1000000 Tokens` and not `1M tokens`.

## Attribution

This is what a provider invoice cannot give you. Every charge row carries the account structure and the dimensions the request was made under.

| FOCUS column | INFRO value |
| --- | --- |
| `InvoiceIssuerName` | The provider that will invoice you — OpenAI, Anthropic, AWS Bedrock. |
| `ServiceProviderName` | The same. The model's *vendor* is `x_ModelVendorName`, which differs on Bedrock and Vertex. |
| `BillingAccountId` / `Name` | The connection: your own provider account, as INFRO holds it. |
| `BillingAccountType` | `<provider> account`. |
| `SubAccountId` / `Name` | The INFRO project, when the request had one. |
| `SubAccountType` | `INFRO project`. |
| `ServiceName` | `<provider> Text Generation`, `Image Generation`, `Video Generation` or `Audio`. |
| `ServiceCategory` | `AI and Machine Learning`. |
| `ServiceSubcategory` | `Generative AI`. |
| `ChargeCategory` | `Usage`, or `Adjustment` on a rounding row. |
| `ChargeFrequency` | `Usage-Based`. |
| `SkuId` / `SkuPriceId` | `<provider>:<model>:<component>`, plus the rate for the price id. |
| `Tags` | Your own activated metadata keys, plus INFRO's derived dimensions under the `infro:` prefix. |

### Tags

Name the metadata keys you want broken out as cost-allocation dimensions with `tag_keys`, the way you would activate a cost allocation tag with a cloud provider. Up to eight, each up to 64 characters of letters, digits, `_`, `.`, `:` and `-`; each one multiplies the row count. A key you do not activate is not a dimension and is not published.

`tag_keys` reads **top-level** keys of the request's `metadata` object (`metadata.team`), not keys nested under `metadata.tags`. The request log's `tag.<key>` filter reads `metadata.tags.<key>`, so a value you filter on there is not picked up by `tag_keys`.

FOCUS requires provider-defined tag keys to carry a provider-specific prefix, so INFRO's derived dimensions are `infro:project`, `infro:environment`, `infro:key`, `infro:route`, `infro:model`, `infro:modality`, `infro:connection`, `infro:workflow`, `infro:feature` and `infro:end-user`. Your own keys carry no prefix. A metadata key of yours that already begins with `infro:` is **dropped** rather than renamed, and reported in `dropped_tag_keys`.

## Custom columns

FOCUS reserves the `x_` prefix for columns outside the specification and requires each to be publicly documented. These are INFRO's.

| Column | Meaning |
| --- | --- |
| `x_InfroSchemaVersionId` | The export contract version. Also on every row, because a CSV file has no envelope. |
| `x_InfroOrganizationId` | Your INFRO organization. |
| `x_InfroConnectionId` / `Name` | The provider connection that served the requests. |
| `x_InfroProviderKindId` | The provider kind: `openai`, `anthropic`, `aws-bedrock`. |
| `x_InfroProjectId` | The project, unresolved. |
| `x_InfroApiKeyId` / `Name` | The gateway key. |
| `x_InfroEnvironmentName` | The key's environment at request time. |
| `x_InfroRouteId` / `Name` | The route, when the request named one. |
| `x_InfroModelId` | The INFRO model slug. |
| `x_InfroFeatureName` | The product feature the requests served, from `metadata.feature` or `X-INFRO-Feature`. Empty when none was sent. |
| `x_ModelVendorName` | The model's maker. Differs from the invoice issuer when a cloud platform serves another vendor's model. |
| `x_InfroModalityName` | `text`, `image`, `video` or `audio`. |
| `x_InfroChargeComponentName` | Which priced component this row is. |
| `x_InfroPriceSourceName` | `list`, `custom` or `custom_provider` — which rung of the rate ladder priced it — or `provider` when the provider reported the request's cost itself. |
| `x_InfroListRateBasisName` | `Recorded` when `ListUnitPrice` is a real list rate; `NotRecorded` when it is unknown. See below. |
| `x_InfroCostBasisName` | `Estimated` or `ProviderReported`. The single field that says whether a row's money came from an invoice. |
| `x_InfroEstimatedCost` | INFRO's own estimate, present even in reconciled mode so the variance is readable. |
| `x_InfroAllocationRatio` | The share of the provider-day this row was allocated, in reconciled mode. |
| `x_InfroRequestCount` | Requests aggregated into the row. |
| `x_InfroWorkflowName` / `x_InfroEndUserId` | The workflow and end-user id the request carried (`metadata.workflow` / `user_id`). Null when it carried none. Also on unpriced rows. |
| `x_InfroRegionPolicyId` | The region *policy* the request ran under. Deliberately not `RegionId` — see below. |
| `x_InfroUnpricedReasonName` | On unpriced rows only. `NoRateOnRecord`. |

## Known limits, stated

- **`ListUnitPrice` is null on a contracted row.** When a request was priced at your negotiated rate, INFRO records that rate and not the vendor's list rate beside it — re-deriving one from today's catalog would rewrite history. `ListCost` then equals `ContractedCost`, so a negotiated discount reads as no discount rather than as an invented one. `x_InfroListRateBasisName` says which you are looking at.
- **Reconciled figures are organization-wide.** The provider reports one figure per provider per day for the whole organization. Each row receives it in proportion to its share of the organization's estimate for that day, independent of paging, so an export scoped to a project or key receives only that scope's share, and `BilledCost` summed over every page of an organization-scoped export equals the reported figure exactly. A scoped export's total may fall short of its exact share by under a micro-dollar per row, never above it. `x_InfroAllocationRatio` is the row's share of the organization's day.
- **`BillingPeriodStart` / `End` are the UTC calendar month** containing the charge period. INFRO does not receive your provider's billing calendar; if yours is not calendar-month, treat these as a grouping key rather than as your invoice period.
- **`RegionId` is not emitted.** `x_InfroRegionPolicyId` records the region policy a request ran under — where INFRO asked for the work to run. FOCUS `RegionId` means where the charge was incurred, and it is not a claim INFRO can make.
- **`ResourceId`, `ResourceName` and `ResourceType` are null.** Inference provisions no resource. The route is an INFRO construct rather than a provider resource, so it is a tag.
- **Commitment discount columns are null.** Provider commitments and reserved capacity are not modelled.
- **USD only.** A provider account billed in another currency is a gap, not a conversion.
- **Only served requests appear.** Charge rows are requests served on one of your provider connections. Every request recorded with an HTTP status of 400 or above is excluded — refusals such as a 402, 403 or 429, and requests that failed.

## Request

- `from` (ISO 8601) — Start of the window. Defaults to 30 days before `to`.
- `to` (ISO 8601) — End of the window, exclusive. Defaults to now.
- `charge_period` (string) — `hour`, `day` (default) or `month`.
- `cost_basis` (string) — `estimated` (default) or `reconciled`.
- `tag_keys` (string) — Comma-separated metadata keys to break out as dimensions. Up to 8.
- `format` (string) — `json` (default) or `csv`.
- `unpriced` (boolean) — With `format=csv`, download the unpriced rows instead of the charge rows.
- `limit` (integer) — Charge rows per page, 1–10,000. Default 1,000.
- `cursor` (string) — From `page.next_cursor`. Keyset; stable across pages.

The window may not exceed 400 days, and is clamped to your plan's request-history retention — the response reports the window actually queried and, when it moved, the one you asked for.

**cURL**

```bash
curl -G https://api.infro.io/v1/usage/focus \
  -H "Authorization: Bearer $INFRO_API_KEY" \
  -d from=2026-09-01T00:00:00Z \
  -d to=2026-10-01T00:00:00Z \
  -d charge_period=day \
  -d tag_keys=team,feature
```

**CSV**

```bash
curl -G https://api.infro.io/v1/usage/focus \
  -H "Authorization: Bearer $INFRO_API_KEY" \
  -d from=2026-09-01T00:00:00Z \
  -d to=2026-10-01T00:00:00Z \
  -d format=csv \
  -o infro-focus-2026-09.csv
```

**Python**

```python
import os, requests

rows, cursor = [], None
while True:
    params = {
        "from": "2026-09-01T00:00:00Z",
        "to": "2026-10-01T00:00:00Z",
        "charge_period": "day",
    }
    if cursor:
        params["cursor"] = cursor

    body = requests.get(
        "https://api.infro.io/v1/usage/focus",
        headers={"Authorization": f"Bearer {os.environ['INFRO_API_KEY']}"},
        params=params,
        timeout=60,
    ).json()

    rows.extend(body["data"])
    cursor = body["page"]["next_cursor"]
    if not cursor:
        break

# Costs are decimal STRINGS, never JSON numbers. Parse with Decimal, not float.
from decimal import Decimal
total = sum(Decimal(row["ListCost"]) for row in rows)
print(f"{len(rows)} charges, estimated {total} USD")

# What the total does not cover, stated rather than inferred.
print("unpriced requests:", body["unpriced"]["summary"]["requests"])
```

## Response

```json
{
  "focus": {
    "version": "1.4",
    "schema_version": "2026-09-20",
    "conformance": "partial",
    "omitted_columns": [
      {
        "column": "BilledCost",
        "reason": "INFRO is not the invoice issuer for model usage. Your provider invoices you directly, and BilledCost MUST reflect amounts as invoiced, not estimated or inferred values. Use cost_basis=reconciled for provider-reported figures, or read ListCost and ContractedCost."
      },
      { "column": "EffectiveCost", "reason": "Omitted with BilledCost: EffectiveCost equals BilledCost for these charges." }
    ],
    "cost_basis": "estimated",
    "charge_period": "day",
    "currency": "USD"
  },
  "window": {
    "from": "2026-09-01T00:00:00.000Z",
    "to": "2026-10-01T00:00:00.000Z",
    "requested_from": null,
    "retention_days": 30
  },
  "data": [
    {
      "InvoiceIssuerName": "Anthropic",
      "BillingAccountId": "conn_4Fh2Kq",
      "BillingAccountName": "Production Anthropic",
      "SubAccountId": "proj_search",
      "ChargePeriodStart": "2026-09-19T00:00:00Z",
      "ChargePeriodEnd": "2026-09-20T00:00:00Z",
      "ServiceName": "Anthropic Text Generation",
      "ServiceCategory": "AI and Machine Learning",
      "ServiceSubcategory": "Generative AI",
      "ChargeCategory": "Usage",
      "ChargeDescription": "anthropic/claude-opus-5 on Anthropic — input tokens",
      "SkuPriceId": "anthropic:anthropic/claude-opus-5:input:5000000/1M tokens",
      "PricingQuantity": "12.481903",
      "PricingUnit": "1000000 Tokens",
      "ConsumedQuantity": "12481903.000000",
      "ConsumedUnit": "Tokens",
      "ListUnitPrice": "5.000000",
      "ContractedUnitPrice": "5.000000",
      "ListCost": "62.409515",
      "ContractedCost": "62.409515",
      "Tags": "{\"team\":\"search\",\"infro:project\":\"Search\",\"infro:environment\":\"production\"}",
      "x_InfroCostBasisName": "Estimated",
      "x_InfroChargeComponentName": "input",
      "x_InfroRequestCount": "3184"
    }
  ],
  "unpriced": { "summary": { "requests": 0, "rows": 0, "models": [] }, "data": [] },
  "excluded": { "zero_cost_requests": 0, "unreconciled_days": [] },
  "dropped_tag_keys": [],
  "page": { "limit": 1000, "next_cursor": null }
}
```

> **Warning:** Every money and quantity value is a **decimal string**, never a JSON number. A JSON number is a double, and summing a million of them gives a different total than summing decimals. Parse with your language's decimal type.

## Versioning

`focus.schema_version` is the export's own contract version, independent of the FOCUS version. It is bumped when a column's meaning changes, a column is removed, or the grain moves. Adding a column — including a new `x_` column — does not bump it, so pin to a version but do not fail on an unrecognised column.

| Response field | Header (CSV) | Meaning |
| --- | --- | --- |
| `focus.version` | `X-INFRO-FOCUS-Version` | The FOCUS release this dataset targets. |
| `focus.schema_version` | `X-INFRO-FOCUS-Schema-Version` | The export contract version. Also a column. |
| `focus.conformance` | `X-INFRO-FOCUS-Conformance` | `conformant` or `partial`. |
| `focus.cost_basis` | `X-INFRO-FOCUS-Cost-Basis` | `estimated` or `reconciled`. |
| `page.next_cursor` | `X-INFRO-Next-Cursor` | Absent on the last page. |
| `unpriced.summary.requests` | `X-INFRO-Unpriced-Requests` | What the file's totals do not cover. Not sent on the `unpriced=true` download. |

## Access

A gateway key needs request-history access (`scoped` or `organization`) — a key's inference reach is not its read reach, and a FOCUS-shaped read is still a read. A `scoped` key returns its own project, or its own requests when it has no project, narrowed to its environment. In the console the same dataset downloads from Analytics and needs `analytics.read`.

See [Authentication](https://infro.io/docs/authentication) for key scopes, and [Errors](https://infro.io/docs/api/errors) for the refusal shapes.
