The OpenAI Costs endpoint, explained
Last verified
The Costs endpoint returns your organisation's spend in daily buckets. It needs an admin key, and it lives at https://api.openai.com/v1/organization/costs.
curl -s "https://api.openai.com/v1/organization/costs?start_time=1717200000&limit=31" \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY" | jq
Parameters
| Parameter | What it does |
|---|---|
start_time | Unix timestamp — the beginning of the window. Required |
end_time | Unix timestamp — the end of the window |
limit | Number of buckets to return |
group_by | Break results down, e.g. by project |
project_ids | Restrict to specific projects |
page | Cursor for paging through results |
start_time being a Unix timestamp rather than a date string is the most common source of confusion. Generate it rather than typing it:
# 30 days ago, portable enough for macOS and Linux
START=$(python3 -c 'import time; print(int(time.time()) - 30*86400)')
curl -s "https://api.openai.com/v1/organization/costs?start_time=$START&limit=31" \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY" | jq
The shape of the response
Results come back as time buckets, each containing result rows with an amount. Roughly:
{
"object": "page",
"data": [
{
"object": "bucket",
"start_time": 1717200000,
"end_time": 1717286400,
"results": [
{
"object": "organization.costs.result",
"amount": { "value": 12.4713, "currency": "usd" },
"line_item": null,
"project_id": null
}
]
}
],
"has_more": true,
"next_page": "..."
}
Two things to handle rather than assume:
- A bucket can have zero results. A day with no spend returns an empty
resultsarray, not a zero row. Summing without guarding produces confusing gaps. amount.valueis a float in dollars, not cents. Sub-cent daily totals are normal on small projects — round for display at the very end, never in the arithmetic, or a real $0.004 day becomes a misleading$0.00.
Grouping
group_by=project_id is what makes per-team or per-product cost attribution possible without a proxy:
curl -s "https://api.openai.com/v1/organization/costs?start_time=$START&limit=31&group_by=project_id" \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY" | jq
This is the practical argument for organising work into projects early. Retrofitting attribution onto a single shared project is not possible after the fact — the data simply isn't there.
Costs vs Usage
Two different endpoints for two different questions:
| Endpoint | Returns | Use it for |
|---|---|---|
/v1/organization/costs | Dollar amounts, daily | Billing, budgets, alerts |
/v1/organization/completions (usage) | Token counts, finer granularity | Optimisation, model comparison |
If the question is "are we about to be surprised by an invoice", you want Costs. If it is "which prompt is too long", you want Usage. See the OpenAI Usage API.
The cadence caveat
Costs data is daily, and recent usage takes time to land. This is not a bug and it is not unique to OpenAI — every provider's billing API works this way, because billing is an accounting system, not a telemetry system.
Consequences worth designing around:
- You cannot build a real-time spend alarm from billing APIs. Anything claiming to be real-time is either proxying your traffic or estimating from token counts.
- A clean-looking chart immediately after a suspected leak proves nothing. Check tomorrow.
- Daily is genuinely sufficient for the thing most people actually need: knowing before the invoice rather than after it.
More on this in why provider billing APIs are daily.