DEV Community

Sukhpinder Singh
Sukhpinder Singh

Posted on

OpenAI Usage API api_key_id: Reconcile Tokens and Costs by Key

OpenAI Usage API api_key_id grouping solves a practical reporting gap: I can see which API key produced completion-token activity and which key accumulated cost. The tricky part is not making the two requests. It is joining their daily buckets without dropping unattributed or unmatched data.

I want a reconciliation report to expose gaps, not smooth them over. A missing cost row, a cost-only row, or a null key ID can each be useful evidence. This pattern keeps those cases visible with a deterministic .NET sample that needs no credentials or paid calls.

Why OpenAI Usage API api_key_id needs a full-outer join

OpenAI's August 4, 2026 API changelog added API-key filtering and grouping to the usage and cost APIs. That gives both responses a shared operational dimension, but it does not make them identical datasets.

The completions usage endpoint reports measures such as input tokens, output tokens, and model requests. Its api_key_id can be null. The costs endpoint returns monetary amounts and currency, also with a nullable API-key dimension.

An inner join would retain only rows present in both responses. That is attractive for a tidy chart, but unsafe for reconciliation. It can hide a key that has token usage but no matching cost row, a key with cost but no completion row, or an unattributed bucket.

I use a full-outer join keyed by (start_time, end_time, api_key_id) instead. Null or blank IDs become an explicit display value such as <unattributed>; they do not disappear.

Query both APIs at the same daily grain

The Costs API supports daily buckets, so I request bucket_width=1d from both endpoints. I also group by the same single dimension:

GET /v1/organization/usage/completions
    ?start_time=...
    &end_time=...
    &bucket_width=1d
    &group_by=api_key_id

GET /v1/organization/costs
    ?start_time=...
    &end_time=...
    &bucket_width=1d
    &group_by=api_key_id
Enter fullscreen mode Exit fullscreen mode

Both resources paginate with has_more and next_page. I keep requesting pages until has_more is false. If a response says more data exists but omits its cursor, I fail the report rather than accepting a partial period. I also reject a repeated cursor to prevent a stuck pagination loop.

This alignment matters. Joining hourly usage against daily cost would manufacture mismatches. Adding model to only the usage grouping would make its row grain incompatible with the cost side. For this report, both sources must resolve to one row per UTC day and API-key ID.

Reconcile tokens and costs without inventing a price

After loading every page, the implementation creates separate indexes for usage and cost. It then unions their keys and assigns a status to every row:

var keys = usage.Keys
    .Concat(costs.Keys)
    .Distinct()
    .OrderBy(key => key.StartTime)
    .ThenBy(key => key.ApiKeyId, StringComparer.Ordinal);

var status = (hasUsage, hasCost) switch
{
    (true, true)  => ReconciliationStatus.Matched,
    (true, false) => ReconciliationStatus.UsageOnly,
    (false, true) => ReconciliationStatus.CostOnly
};
Enter fullscreen mode Exit fullscreen mode

The indexes reject duplicate day/key pairs. That catches accidental extra grouping dimensions or repeated pages before they inflate totals. The cost index also uses decimal, retains the returned currency, and refuses to combine multiple currencies in one report.

The important boundary is that token counts and billed amount remain separate measures. I do not multiply tokens by a model price and compare that estimate with the Costs API. A cost bucket may contain line items beyond completion tokens, so equality would be an unsupported assumption.

The runnable sample on main uses two synthetic pages from each API. Its output contains four matched rows, one usage-only row, and one cost-only row. It also preserves a null key bucket as <unattributed>.

Nine offline checks verify pagination, the six-row full-outer result, status counts, null handling, separate token and cost measures, duplicate rejection, currency validation, and daily bucket alignment. The merged pull request records the exact restore, format, build, run, and dependency checks.

When this pattern is not enough

API-key grouping is useful for operational ownership, migration tracking, and coarse chargeback. It is not request-level attribution. If one key serves several products or customers, add your own request metadata and internal ledger at call time. Do not expect the organization usage report to reconstruct that boundary later.

A live collector also needs an organization Admin API key. Keep it in a secret manager or environment variable, never in source, logs, or fixtures. The sample avoids that risk by making no network requests; its identifiers and amounts are synthetic.

For a quick manual check, the dashboard may be enough. For repeatable reporting, I prefer the API plus explicit failure states: incomplete pagination, unmatched rows, unattributed activity, duplicate dimensions, and mixed currency should all be visible before anyone trusts a total.

Which gap would you alert on first: usage-only, cost-only, or unattributed?

Happy coding!

Top comments (0)