OpenAI has two admin endpoints for spend data. GET /v1/organization/costs returns what you were billed, in US dollars, in daily buckets. GET /v1/organization/usage/completions and its siblings return tokens and request counts in buckets of a minute, an hour or a day. Both need an organization Admin key, both can group results by project and API key, and longer ranges come back in pages that you follow with next_page.
Get an Admin key
Both endpoints only accept an organization Admin key, which starts with sk-admin-. A project key (sk-proj-) won't work. Only organization owners can create one, under Organization settings, then Admin keys, then "Create new admin key". For reporting, give it read-only permissions, because an Admin key with write access can create projects, service accounts and keys. Admin keys can't call models. Our post on creating a read-only OpenAI Admin API key has the steps.
Keep the key in an environment variable or a secret store, and out of your code:
export OPENAI_ADMIN_KEY="sk-admin-..."Get daily costs with the Costs API
The Costs API takes these query parameters:
start_time: Unix seconds, inclusive. Required.end_time: Unix seconds, exclusive.bucket_width: only1dis supported.group_by: any ofproject_id,api_key_id,line_item,user_idandapi_source.project_ids,api_key_idsandline_items: filters.limit: buckets per page, from 1 to 180. The default is 7.page: the cursor from the previous response'snext_page.
This request gets 30 days of cost from October 1, 2026 (1790812800 in Unix time), by project and line item:
curl "https://api.openai.com/v1/organization/costs?start_time=1790812800&limit=30&group_by=project_id&group_by=line_item" \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY" To work out a start time in a shell, date -u -d '30 days ago' +%s works on Linux and date -u -v-30d +%s on macOS. The response is a page of daily buckets (example values):
{
"object": "page",
"data": [
{
"object": "bucket",
"start_time": 1790812800,
"end_time": 1790899200,
"results": [
{
"object": "organization.costs.result",
"amount": { "value": 41.27, "currency": "usd" },
"line_item": "gpt-6-astra, input_tokens",
"project_id": "proj_abc123"
}
]
}
],
"has_more": true,
"next_page": "page_..."
} The dollar figure is in amount.value. With line-item grouping, each result also has a quantity and a quantity_unit.
Get tokens and requests with the Usage API
Usage is split by kind of model call. /v1/organization/usage/completions covers text generation, and there are sibling endpoints for embeddings, moderations, images, audio_speeches, audio_transcriptions and code_interpreter_sessions. The parameters work like the Costs API's, with these differences:
bucket_widthcan be1m,1hor1d(the default), with at most 1,440, 168 or 31 buckets per page respectively.group_byacceptsproject_id,user_id,api_key_id,model,batch,service_tierandapi_source.- Besides projects and keys, you can filter by
models,user_idsandbatch.
This request gets hourly usage by model for November 10, 2026:
curl "https://api.openai.com/v1/organization/usage/completions?start_time=1794268800&bucket_width=1h&limit=24&group_by=model" \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY"Each result in a bucket looks like this (example values):
{
"object": "organization.usage.completions.result",
"input_tokens": 1843210,
"input_cached_tokens": 912000,
"output_tokens": 204118,
"num_model_requests": 611,
"model": "gpt-6.1-terra",
"project_id": null,
"api_key_id": null
}input_tokens includes cached input and cache writes, and the optional input_uncached_tokens and input_cache_write_tokens fields break it down where they're present. Fields you didn't group by come back as null.
Page through results in Python
Each call returns one page of buckets. Keep calling with page set to the previous next_page until has_more is false. This script uses requests rather than an SDK. It gets the last 30 days of cost by project and of usage by project and model, then prints dollars per project and tokens per request:
import os
from collections import defaultdict
from datetime import datetime, timedelta, timezone
import requests
BASE = "https://api.openai.com/v1/organization"
HEADERS = {"Authorization": f"Bearer {os.environ['OPENAI_ADMIN_KEY']}"}
def get_buckets(path, params):
"""Fetch every page of buckets from a costs or usage endpoint."""
buckets, page = [], None
while True:
query = dict(params, page=page) if page else params
r = requests.get(f"{BASE}/{path}", headers=HEADERS, params=query, timeout=30)
r.raise_for_status()
body = r.json()
buckets.extend(body["data"])
if not body.get("has_more"):
return buckets
page = body["next_page"]
# start at midnight UTC so each daily bucket is one calendar day
today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
start = int((today - timedelta(days=30)).timestamp())
# dollars by project
cost = defaultdict(float)
for bucket in get_buckets("costs", {"start_time": start, "group_by": ["project_id"], "limit": 31}):
for row in bucket["results"]:
amount = row.get("amount") or {}
cost[row.get("project_id") or "(no project)"] += amount.get("value", 0)
# requests and tokens by project and model
usage = get_buckets(
"usage/completions",
{"start_time": start, "bucket_width": "1d", "group_by": ["project_id", "model"], "limit": 31},
)
totals = defaultdict(lambda: [0, 0, 0]) # requests, input tokens, output tokens
for bucket in usage:
for row in bucket["results"]:
t = totals[(row.get("project_id") or "(no project)", row.get("model"))]
t[0] += row["num_model_requests"]
t[1] += row["input_tokens"]
t[2] += row["output_tokens"]
for project, dollars in sorted(cost.items(), key=lambda kv: kv[1], reverse=True):
print(f"{project}: ${dollars:,.2f}")
for (project, model), (n, tokens_in, tokens_out) in totals.items():
if n:
print(f"{project} {model}: {n} requests, {tokens_in // n} in and {tokens_out // n} out per request")requests sends a list such as ["project_id", "model"] as repeated group_by parameters, the same form as the curl examples above.
Turn project and key IDs into names
Results carry IDs such as proj_… and key_…. Look up the names with the same Admin key. GET /v1/organization/projects lists projects, and GET /v1/organization/projects/{project_id}/api_keys lists a project's keys with their names, owners and when each was last used. These list endpoints page differently from the reports. They return up to 100 items per page, and you pass the previous page's last_id as after. Include archived projects, or costs from an archived project won't have a name to match:
def project_names():
"""Map project IDs to names, archived projects included."""
names, after = {}, None
while True:
params = {"limit": 100, "include_archived": "true"}
if after:
params["after"] = after
r = requests.get(f"{BASE}/projects", headers=HEADERS, params=params, timeout=30)
r.raise_for_status()
body = r.json()
names.update((p["id"], p.get("name") or p["id"]) for p in body["data"])
if not body.get("has_more"):
return names
after = body["last_id"]Names change far less often than spend, so cache the lookup.
Common mistakes
Treating the last day or two as final
OpenAI doesn't document how quickly usage and cost data settle, and recent days can still change. On every run, fetch the last few days again and overwrite them, instead of only appending new days.
Dropping rows with no project or key
Some results come back with an empty project_id or api_key_id. Keep them in an unassigned row, or your totals won't match the organization's.
Multiplying tokens by list prices and calling it cost
Cached input, Batch and Flex pricing, Fast mode, prompts over 272K tokens and tool charges all make a plain multiplication wrong. Take dollars from the Costs API and use the Usage API to explain them.
Grouping costs by user and project together
In the Costs API, combining user_id with project grouping can return a 400 error, depending on the organization and the period. Run them as separate requests.
Mixing time zones
Timestamps are Unix seconds and the dashboard shows UTC dates. Start queries at midnight UTC so each daily bucket is one calendar day, and label reports as UTC so "yesterday" means the same day to everyone reading.
Relying on the API to keep your history
OpenAI doesn't document how far back the data goes. If you need a long history, store what you fetch in your own table, keyed by day and group, so revised days overwrite older figures.
Common questions
Can I use a normal API key with the OpenAI Usage API?
No. The Usage and Costs endpoints need an organization Admin key, which starts with sk-admin-, and only organization owners can create one. Project keys starting with sk-proj- are for calling models.
How far back does the OpenAI Usage API go?
OpenAI doesn't document a limit. If you need a long history, fetch the data regularly and keep your own copy.
How up to date is OpenAI usage and cost data?
OpenAI doesn't publish a freshness guarantee for either API, and the most recent days can still change. Treat today and yesterday as provisional and fetch them again later.
Can I get OpenAI cost by model from the API?
Not directly. The Costs API doesn't group by model, but its line items name the model and the kind of charge, so group by line_item and add up each model's lines. The Usage API groups tokens and requests by model, and our post on seeing OpenAI costs by project, key and model covers the dashboard views as well.
Where Braid fits
If you'd rather not run and maintain this job yourself, Braid reads the same cost and usage data with a read-only Admin key. It keeps the mapping from projects and keys to the use cases your company names, each with an owner and a monthly budget, and sends a weekly report of what changed.
It never reads prompts or responses and never changes your OpenAI account. The beta currently supports OpenAI API accounts. More provider connections are planned.