The Anthropic Admin API gives you read-only programmatic access to organization-level data: token usage, workspaces, members, and audit logs. It does not create messages — that is the regular Messages API. Admin keys carry the prefix sk-ant-admin-... (distinct from the message-sending sk-ant-api03-... keys) and authenticate to a separate surface area used by cost-tracking dashboards, finance integrations, and provisioning scripts. The three endpoints you will use 90% of the time are GET /v1/organizations, GET /v1/organizations/{id}/usage_report/messages, and GET /v1/organizations/{id}/workspaces. Rate limits on the Admin API are gentler than the Messages API because the calls are infrequent — a nightly cron is the typical pattern.
Admin API vs regular API
These are two different products with two different keys. Mixing them up is the most common first-day mistake.
| Capability | Messages API (sk-ant-api03-...) |
Admin API (sk-ant-admin-...) |
|---|---|---|
| Send messages to Claude | Yes | No |
| Stream completions | Yes | No |
| Use tools / files / batch | Yes | No |
| List organizations | No | Yes |
| Pull usage_report (token + cost data) | No | Yes |
| List/create workspaces | No | Yes |
| List/invite organization members | No | Yes |
| Read audit logs | No | Yes |
| Counts toward your message rate limits | Yes | No |
Required anthropic-version header |
Yes | Yes |
If your code calls client.messages.create(...), you need a regular key. If your code is building a Grafana panel of "tokens spent yesterday by workspace," you need an admin key. Most production setups have both, stored as separate environment variables (ANTHROPIC_API_KEY and ANTHROPIC_ADMIN_KEY).
Generate an Admin API key
Admin keys are not visible from the regular API Keys page — they live one level up.
- Sign in at
console.anthropic.comas an organization owner (admin keys cannot be created by member-tier users). - Open Settings → Admin Keys.
- Click Create Admin Key, give it a descriptive name (e.g.
cost-tracker-prod), and copy the value once. You cannot view it again. - Store it as
ANTHROPIC_ADMIN_KEYin your secret manager.
Admin keys are scoped to the organization that created them. They cannot be limited to a single workspace from the console — you scope by workspace at query time using the workspace_ids[] parameter on the usage endpoint.
Authentication
Every Admin API request needs two headers:
curl https://api.anthropic.com/v1/organizations \
-H "x-api-key: $ANTHROPIC_ADMIN_KEY" \
-H "anthropic-version: 2023-06-01"
The anthropic-version value is the same one used by the Messages API — 2023-06-01 is the current GA value as of May 2026. If you omit it, requests return 400 invalid_request_error.
GET /v1/organizations
Returns the organizations the key can see. For most accounts this is a single org, but enterprise setups with multiple billing entities will see more than one.
{
"data": [
{
"id": "org_01ABCxyz...",
"name": "Acme Corp",
"created_at": "2024-08-12T18:42:11Z"
}
]
}
Cache the id — every other admin call needs it in the path.
GET /v1/organizations/{id}/usage_report/messages
This is the heavy hitter. It returns token consumption bucketed by time, model, and (optionally) workspace.
Query parameters:
| Param | Required | Notes |
|---|---|---|
starting_at |
yes | RFC 3339 timestamp, inclusive. Max lookback ~13 months. |
ending_at |
no | Defaults to "now". Exclusive. |
bucket_width |
no | 1d (default) or 1h. 1m is not supported. |
workspace_ids[] |
no | Filter to one or more workspaces. Repeat the param. |
models[] |
no | Filter to specific models, e.g. claude-opus-4-7. |
Response shape:
{
"data": [
{
"starting_at": "2026-05-08T00:00:00Z",
"ending_at": "2026-05-09T00:00:00Z",
"results": [
{
"model": "claude-sonnet-4-7",
"workspace_id": "wrkspc_01XYZ...",
"input_tokens": 1842110,
"output_tokens": 312044,
"cache_creation_input_tokens": 88210,
"cache_read_input_tokens": 1488221,
"service_tier": "standard"
}
]
}
],
"has_more": false
}
Pagination: as of May 2026 the usage endpoint returns the entire window in a single response — has_more is reserved for future use but currently always false. That said, write your loop assuming pagination will land; you do not want to rewrite the integration when it does.
The four token fields matter independently because they price differently:
input_tokens— base input rate.output_tokens— base output rate (about 5× input).cache_creation_input_tokens— 1.25× input rate (one-time write cost).cache_read_input_tokens— 0.1× input rate (the savings you came for).
Sum them with the right multipliers or your "total cost" number will be off by 30%+ on cache-heavy workloads.
GET /v1/organizations/{id}/workspaces
curl "https://api.anthropic.com/v1/organizations/$ORG_ID/workspaces" \
-H "x-api-key: $ANTHROPIC_ADMIN_KEY" \
-H "anthropic-version: 2023-06-01"
Returns workspace IDs and display names. Use this to build a workspace_id → label lookup before you render any usage chart, otherwise your dashboard shows opaque wrkspc_01... strings.
Worked example: Python script for 30-day CSV
The script below pulls 30 days of usage, joins it with model pricing, and writes a CSV. Drop it on a cron and you have a finance feed.
#!/usr/bin/env python3
"""Pull 30-day Anthropic usage and emit cost CSV."""
import csv
import os
from datetime import datetime, timedelta, timezone
import requests
ADMIN_KEY = os.environ["ANTHROPIC_ADMIN_KEY"]
BASE = "https://api.anthropic.com/v1"
HEADERS = {"x-api-key": ADMIN_KEY, "anthropic-version": "2023-06-01"}
# Pricing per million tokens — refresh from claude-api-pricing-2026 quarterly.
PRICING = {
"claude-opus-4-7": {"in": 15.00, "out": 75.00, "cw": 18.75, "cr": 1.50},
"claude-sonnet-4-7": {"in": 3.00, "out": 15.00, "cw": 3.75, "cr": 0.30},
"claude-haiku-4-5": {"in": 0.80, "out": 4.00, "cw": 1.00, "cr": 0.08},
}
def org_id() -> str:
r = requests.get(f"{BASE}/organizations", headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json()["data"][0]["id"]
def workspaces(org: str) -> dict[str, str]:
r = requests.get(f"{BASE}/organizations/{org}/workspaces",
headers=HEADERS, timeout=30)
r.raise_for_status()
return {w["id"]: w["name"] for w in r.json()["data"]}
def usage(org: str, days: int = 30) -> list[dict]:
end = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
start = end - timedelta(days=days)
params = {
"starting_at": start.isoformat().replace("+00:00", "Z"),
"ending_at": end.isoformat().replace("+00:00", "Z"),
"bucket_width": "1d",
}
r = requests.get(f"{BASE}/organizations/{org}/usage_report/messages",
headers=HEADERS, params=params, timeout=60)
r.raise_for_status()
return r.json()["data"]
def cost(row: dict) -> float:
p = PRICING.get(row["model"])
if not p:
return 0.0
return (
row["input_tokens"] * p["in"] +
row["output_tokens"] * p["out"] +
row["cache_creation_input_tokens"] * p["cw"] +
row["cache_read_input_tokens"] * p["cr"]
) / 1_000_000
def main() -> None:
org = org_id()
wmap = workspaces(org)
rows = []
for bucket in usage(org):
for r in bucket["results"]:
rows.append({
"date": bucket["starting_at"][:10],
"workspace": wmap.get(r["workspace_id"], r["workspace_id"]),
"model": r["model"],
"input": r["input_tokens"],
"output": r["output_tokens"],
"cache_write": r["cache_creation_input_tokens"],
"cache_read": r["cache_read_input_tokens"],
"usd": round(cost(r), 4),
})
with open("anthropic_usage.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader()
w.writerows(rows)
print(f"wrote {len(rows)} rows, total ${sum(r['usd'] for r in rows):.2f}")
if __name__ == "__main__":
main()
Want this without writing a single line of code? claudecosts.app is a free dashboard built on top of this exact API — paste your admin key, get daily usage, per-workspace breakdowns, and budget alerts. Use it as your reference implementation if you decide to roll your own.
Worked example: TypeScript fetch with retry + budget gate
For Node/edge runtimes, here is a typed version with exponential backoff and a hard budget cutoff that makes the script return non-zero if you blow your monthly cap.
// usage-guard.ts — fail CI if month-to-date spend exceeds budget
const ADMIN_KEY = process.env.ANTHROPIC_ADMIN_KEY!;
const BUDGET_USD = Number(process.env.MONTHLY_BUDGET_USD ?? "500");
const BASE = "https://api.anthropic.com/v1";
const HEADERS = {
"x-api-key": ADMIN_KEY,
"anthropic-version": "2023-06-01",
};
const PRICE = {
"claude-opus-4-7": [15, 75, 18.75, 1.5],
"claude-sonnet-4-7": [3, 15, 3.75, 0.3],
"claude-haiku-4-5": [0.8, 4, 1.0, 0.08],
} as const;
async function get<T>(path: string, params?: Record<string, string>): Promise<T> {
const url = new URL(BASE + path);
if (params) Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
for (let attempt = 0; attempt < 4; attempt++) {
const r = await fetch(url, { headers: HEADERS });
if (r.ok) return r.json() as Promise<T>;
if (r.status >= 500 || r.status === 429) {
await new Promise(res => setTimeout(res, 2 ** attempt * 500));
continue;
}
throw new Error(`${r.status} ${await r.text()}`);
}
throw new Error("retries exhausted");
}
const orgs = await get<{ data: { id: string }[] }>("/organizations");
const orgId = orgs.data[0].id;
const start = new Date();
start.setUTCDate(1); start.setUTCHours(0, 0, 0, 0);
const usage = await get<{ data: { results: any[] }[] }>(
`/organizations/${orgId}/usage_report/messages`,
{ starting_at: start.toISOString(), bucket_width: "1d" }
);
let mtd = 0;
for (const bucket of usage.data) for (const r of bucket.results) {
const p = PRICE[r.model as keyof typeof PRICE];
if (!p) continue;
mtd += (r.input_tokens*p[0] + r.output_tokens*p[1]
+ r.cache_creation_input_tokens*p[2]
+ r.cache_read_input_tokens*p[3]) / 1_000_000;
}
console.log(`MTD spend: $${mtd.toFixed(2)} / budget $${BUDGET_USD}`);
if (mtd > BUDGET_USD) {
console.error("BUDGET EXCEEDED");
process.exit(1);
}
Hook this into a CI step that runs hourly and you have a hard cost ceiling.
Common gotchas
A short list of things that have burned at least one engineer on this codebase:
- Admin keys do not appear in the regular API Keys list. They live under Settings → Admin Keys. Searching for them in the wrong tab is the most common "I can't find my key" support ticket.
- The usage_report has a 1–2 hour lag from real time. It is meant for daily reporting, not live throttling. For real-time guardrails, count tokens on the request side from the Messages API response headers.
- Cache tokens are reported separately and priced separately.
cache_creation_input_tokensis more expensive than base input;cache_read_input_tokensis much cheaper. If you treat them as plaininput_tokensyour cost report will diverge from the Anthropic invoice. - Deleted workspaces still show up in historical data by their old
wrkspc_...ID. The/workspacesendpoint returns active workspaces only — pre-build a long-lived cache so that decommissioned workspace IDs still resolve to a label in your dashboard. - Service tiers differ. The
service_tierfield can bestandard,priority, orbatch, each with its own pricing. If you launch Batch API jobs, fold those rows in with batch pricing (typically 50% off standard) or your numbers will be wrong by half on those rows. - No per-key attribution. The usage endpoint reports by workspace, not by individual API key. If you want key-level breakdowns, give each service its own workspace.
Self-hosted dashboard recipe
If you want internal-only and own all the data, the smallest viable stack:
- Cron —
launchdon macOS or a Render/Fly cron worker. Runs the Python script above every six hours. - Postgres — one table,
anthropic_usage(date, workspace, model, input, output, cache_write, cache_read, usd). Upsert by(date, workspace, model)so re-runs are idempotent. - Recharts or Tremor — Next.js page that queries Postgres and renders a stacked area chart of daily USD per workspace plus a per-model breakdown.
- Slack alert — a second cron that compares week-over-week and posts to Slack if spend climbs more than 25%.
This is roughly the architecture behind claudecosts.app, except they ship it as a service so you don't have to run the Postgres. If you'd rather not build it, claudecosts.app does this for free. For the optimization side — actually getting your bill down once you can see it — the Cost Optimization Masterclass covers the same patterns this article hints at: cache shaping, model routing, and batch consolidation.
For deeper background on what the cost numbers mean, see Claude API cost monitoring guide, the current Claude API pricing 2026, and the prompt caching cost benchmark which shows where cache reads start paying for cache writes.
Frequently Asked Questions
Can the Admin API send messages?
No. The Admin API surface is read/management only — organizations, workspaces, members, invites, API keys, audit logs, and usage_report. To call messages.create you need a regular sk-ant-api03-... key. The two key types are not interchangeable: an admin key sent to /v1/messages returns 401 authentication_error and a regular key sent to /v1/organizations returns the same.
How recent is the usage data?
Expect a 1–2 hour lag at the daily bucket and slightly more for hourly. The usage_report is designed for finance and reporting, not for real-time rate limiting. If you need to cut off requests the moment a budget is hit, count tokens client-side from the Messages API response (usage.input_tokens etc.) and store running totals in Redis. Then reconcile against the Admin API daily.
Can I rotate Admin keys without downtime?
Yes. Create a second admin key in the console, deploy it to your secret store under a new name, restart consumers, then revoke the old key. Admin keys do not have a built-in "next" slot like some providers offer, so you maintain rotation by always running with two valid keys overlapping for a few minutes.
How do I scope by workspace?
Pass workspace_ids[] on the usage_report/messages call — repeat the param to list multiple. Example: ?workspace_ids[]=wrkspc_A&workspace_ids[]=wrkspc_B. The response only includes results from those workspaces. There is no per-API-key filter, so the standard pattern is "one workspace per service" if you need fine-grained attribution.
Are there official SDKs?
The standard anthropic Python and @anthropic-ai/sdk Node packages cover the Messages API but Admin API support has lagged — as of May 2026 most teams hit Admin endpoints with plain requests / fetch as shown above. Community wrappers exist on GitHub but the surface is small enough (≈12 endpoints) that hand-rolled typed fetchers are usually less hassle than another dependency.