How to use the OpenAI Usage and Costs API, with examples

How to pull OpenAI usage and cost data with an Admin key: the endpoints, grouping by project, key and model, pagination, and the mistakes people often make.

Braid6-minute read

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: only 1d is supported.
  • group_by: any of project_id, api_key_id, line_item, user_id and api_source.
  • project_ids, api_key_ids and line_items: filters.
  • limit: buckets per page, from 1 to 180. The default is 7.
  • page: the cursor from the previous response's next_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_width can be 1m, 1h or 1d (the default), with at most 1,440, 168 or 31 buckets per page respectively.
  • group_by accepts project_id, user_id, api_key_id, model, batch, service_tier and api_source.
  • Besides projects and keys, you can filter by models, user_ids and batch.

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.

Try Braid with your team.

We're looking for teams to test Braid before launch. Get full access to all premium features for free. All we ask is for feedback on what works and what needs improvement.

  1. Request beta access.Tell us your name, role and company.
  2. Receive your invitation.If approved, you'll get an email inviting you to join.
  3. Set up your company.Connect your provider, map your AI use cases and invite your team.

Request beta access.

Justin, Braid's founder, reviews every request. If yours is approved, you'll get an invitation by email.

Free during beta. No credit card required.