{"slug": "openai-usage-api-api-key-id-reconcile-tokens-and-costs-by-key", "title": "OpenAI Usage API api_key_id: Reconcile Tokens and Costs by Key", "summary": "A developer has published a .NET sample that reconciles OpenAI's Usage and Costs APIs by API key, using a full-outer join to surface gaps such as missing cost rows or unattributed buckets. The approach ensures both endpoints are grouped by day and API key, and it rejects mismatched grains or pagination errors to keep the report deterministic.", "body_md": "OpenAI Usage API `api_key_id`\n\ngrouping 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.\n\nI 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.\n\n`api_key_id`\n\nneeds a full-outer join\nOpenAI's [August 4, 2026 API changelog](https://developers.openai.com/api/docs/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.\n\nThe [completions usage endpoint](https://developers.openai.com/api/reference/python/resources/admin/subresources/organization/subresources/usage/methods/completions) reports measures such as input tokens, output tokens, and model requests. Its `api_key_id`\n\ncan be null. The [costs endpoint](https://developers.openai.com/api/reference/python/resources/admin/subresources/organization/subresources/usage/methods/costs) returns monetary amounts and currency, also with a nullable API-key dimension.\n\nAn 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.\n\nI use a full-outer join keyed by `(start_time, end_time, api_key_id)`\n\ninstead. Null or blank IDs become an explicit display value such as `<unattributed>`\n\n; they do not disappear.\n\nThe Costs API supports daily buckets, so I request `bucket_width=1d`\n\nfrom both endpoints. I also group by the same single dimension:\n\n```\nGET /v1/organization/usage/completions\n    ?start_time=...\n    &end_time=...\n    &bucket_width=1d\n    &group_by=api_key_id\n\nGET /v1/organization/costs\n    ?start_time=...\n    &end_time=...\n    &bucket_width=1d\n    &group_by=api_key_id\n```\n\nBoth resources paginate with `has_more`\n\nand `next_page`\n\n. I keep requesting pages until `has_more`\n\nis 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.\n\nThis alignment matters. Joining hourly usage against daily cost would manufacture mismatches. Adding `model`\n\nto 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.\n\nAfter loading every page, the implementation creates separate indexes for usage and cost. It then unions their keys and assigns a status to every row:\n\n``` js\nvar keys = usage.Keys\n    .Concat(costs.Keys)\n    .Distinct()\n    .OrderBy(key => key.StartTime)\n    .ThenBy(key => key.ApiKeyId, StringComparer.Ordinal);\n\nvar status = (hasUsage, hasCost) switch\n{\n    (true, true)  => ReconciliationStatus.Matched,\n    (true, false) => ReconciliationStatus.UsageOnly,\n    (false, true) => ReconciliationStatus.CostOnly\n};\n```\n\nThe 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`\n\n, retains the returned currency, and refuses to combine multiple currencies in one report.\n\nThe 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.\n\nThe runnable [sample on main](https://github.com/ssukhpinder/dev-to-code-samples/tree/main/113-openai-api-key-usage-costs) 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\n\n`<unattributed>`\n\n.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](https://github.com/ssukhpinder/dev-to-code-samples/pull/103) records the exact restore, format, build, run, and dependency checks.\n\nAPI-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.\n\nA 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.\n\nFor 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.\n\nWhich gap would you alert on first: usage-only, cost-only, or unattributed?\n\nHappy coding!", "url": "https://wpnews.pro/news/openai-usage-api-api-key-id-reconcile-tokens-and-costs-by-key", "canonical_source": "https://dev.to/ssukhpinder/openai-usage-api-apikeyid-reconcile-tokens-and-costs-by-key-4jmo", "published_at": "2026-08-31 15:38:10+00:00", "updated_at": "2026-08-31 15:52:38.522975+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["OpenAI", ".NET", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/openai-usage-api-api-key-id-reconcile-tokens-and-costs-by-key", "markdown": "https://wpnews.pro/news/openai-usage-api-api-key-id-reconcile-tokens-and-costs-by-key.md", "text": "https://wpnews.pro/news/openai-usage-api-api-key-id-reconcile-tokens-and-costs-by-key.txt", "jsonld": "https://wpnews.pro/news/openai-usage-api-api-key-id-reconcile-tokens-and-costs-by-key.jsonld"}}