DocsObservability
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.
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.
The FinOps Open Cost and Usage Specification (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.
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=estimateddefault- Every provider-day in the window.
ListCostandContractedCostare populated;BilledCostandEffectiveCostare omitted from the dataset. The response reportsfocus.conformance: "partial"and names the omitted columns with the reason. This is the mode most organizations can use today. cost_basis=reconciledrequires charge_period=day- Only provider-days for which your provider reported a cost figure.
BilledCostandEffectiveCostare populated from it, andfocus.conformanceis"conformant". Requires a billing credential for that provider, a Business-plan feature available for OpenAI and Anthropic only — see 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.
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.
{
"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.
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 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
ListUnitPriceis 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.ListCostthen equalsContractedCost, so a negotiated discount reads as no discount rather than as an invented one.x_InfroListRateBasisNamesays 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
BilledCostsummed 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_InfroAllocationRatiois the row's share of the organization's day. BillingPeriodStart/Endare 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.RegionIdis not emitted.x_InfroRegionPolicyIdrecords the region policy a request ran under — where INFRO asked for the work to run. FOCUSRegionIdmeans where the charge was incurred, and it is not a claim INFRO can make.ResourceId,ResourceNameandResourceTypeare 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
fromISO 8601- Start of the window. Defaults to 30 days before
to. toISO 8601- End of the window, exclusive. Defaults to now.
charge_periodstringhour,day(default) ormonth.cost_basisstringestimated(default) orreconciled.tag_keysstring- Comma-separated metadata keys to break out as dimensions. Up to 8.
formatstringjson(default) orcsv.unpricedboolean- With
format=csv, download the unpriced rows instead of the charge rows. limitinteger- Charge rows per page, 1–10,000. Default 1,000.
cursorstring- 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 -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,featurecurl -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.csvimport 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
{
"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 }
}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 for key scopes, and Errors for the refusal shapes.