Querying Usage by API Key and Workspace with the Admin API
Filter historical usage and cost data down to a single API key or workspace.
Summary
Most cost questions are not "what did we spend in total," they are "what did this team spend."
The Admin API answers that by letting you filter and group usage and cost reports on api_key_id and workspace_id.
Filtering narrows the result set to one or a few identifiers before the API even computes the report.
Grouping keeps every identifier's rows visible side by side, which is usually the better choice once you have more than one key or workspace to compare.
This page covers both, plus how to resolve a human-readable key or workspace name into the ID the API expects.
Recipe
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_ADMIN_KEY from env
# Filter: usage for one specific API key
by_key = client.beta.usage.report(
starting_at="2026-06-01T00:00:00Z",
api_key_ids=["apikey_01A2B3C4"],
)
# Filter: usage for one specific workspace
by_workspace = client.beta.usage.report(
starting_at="2026-06-01T00:00:00Z",
workspace_ids=["wksp_01abc123"],
)
# Group: usage broken out per key, no filter applied
grouped = client.beta.usage.report(
starting_at="2026-06-01T00:00:00Z",
group_by=["api_key_id"],
)When to reach for this:
- You are investigating a cost spike and suspect it traces back to one integration.
- You need a per-key or per-workspace number for a chargeback or budget report.
- You are auditing which keys are still active before revoking unused ones.
- You want a leaderboard of top spenders across your organization, not a single total.
Working Example
import anthropic
from datetime import datetime, timedelta, timezone
client = anthropic.Anthropic()
def usage_for_key(api_key_id: str, days: int = 30) -> dict:
"""Return summed token usage for one API key over the trailing N days."""
start = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
report = client.beta.usage.report(
starting_at=start,
api_key_ids=[api_key_id],
bucket_width="1d",
)
totals = {
"uncached_input_tokens": 0,
"cached_input_tokens": 0,
"cache_creation_input_tokens": 0,
"output_tokens": 0,
}
for bucket in report.data:
totals["uncached_input_tokens"] += bucket.uncached_input_tokens
totals["cached_input_tokens"] += bucket.cached_input_tokens
totals["cache_creation_input_tokens"] += bucket.cache_creation_input_tokens
totals["output_tokens"] += bucket.output_tokens
return totals
def top_workspaces_by_output(days: int = 30, limit: int = 5) -> list[tuple[str, int]]:
"""Rank workspaces by output token volume over the trailing N days."""
start = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
report = client.beta.usage.report(
starting_at=start,
group_by=["workspace_id"],
)
ranked = sorted(
((bucket.workspace_id, bucket.output_tokens) for bucket in report.data),
key=lambda pair: pair[1],
reverse=True,
)
return ranked[:limit]
if __name__ == "__main__":
print(usage_for_key("apikey_01A2B3C4"))
for workspace_id, output_tokens in top_workspaces_by_output():
print(workspace_id, output_tokens)What this demonstrates:
usage_for_keyshows a single-identifier filter used to answer "how much did this one key cost."top_workspaces_by_outputshows grouping used to rank many identifiers at once, without filtering any of them out.- Both functions accumulate token-type totals separately rather than collapsing them, so the caller can still apply per-type pricing later.
bucket_width="1d"keeps the report from returning one giant summed row when you might want a trend later.
Deep Dive
How It Works
api_key_idsandworkspace_idsare both list-typed filters; passing multiple values returns usage for any of them, not just the first.- Filtering happens before aggregation, so a filtered report is cheaper to reason about and typically faster than fetching everything and filtering client-side.
group_byand the filter parameters compose: you can filter to a handful of workspaces and still group the result by API key within them.- IDs, not names, are what the API accepts. A workspace named "Growth Team" and a workspace ID like
wksp_01abc123are different things, and only the ID is a valid filter value.
Resolving Names to IDs
| You have | You need | How to get it |
|---|---|---|
| A workspace name shown in the Console | workspace_id | List workspaces via the Admin API's workspace endpoint, or copy the ID from the workspace's settings page in the Console |
| A key's display label | api_key_id | List API keys via the Admin API's API keys endpoint; the label and the ID are returned together |
| Only a partial cost total from the Console UI | Both | Cross-reference the Console's usage page, which shows both name and ID when you click into a workspace or key |
import anthropic
client = anthropic.Anthropic()
# Resolve a workspace's human-readable name to the ID the usage API expects
workspaces = client.beta.admin.workspaces.list()
target = next(w for w in workspaces.data if w.name == "Growth Team")
print(target.id) # use this value as a workspace_ids filterPython Notes
# Building a filter list dynamically is a common pattern once key IDs
# come from your own database rather than being hardcoded.
active_key_ids: list[str] = load_active_key_ids_from_db()
report = client.beta.usage.report(
starting_at="2026-06-01T00:00:00Z",
api_key_ids=active_key_ids or None, # None means "no filter" to the SDK
)Parameters & Return Values
| Parameter | Type | Description |
|---|---|---|
api_key_ids | list[str] | Restrict the report to one or more API key IDs. |
workspace_ids | list[str] | Restrict the report to one or more workspace IDs. |
group_by | list[str] | Return one row per unique combination of these dimensions, e.g. ["api_key_id"]. |
starting_at | str (ISO 8601) | Start of the report window, required. |
ending_at | str (ISO 8601) | End of the report window, optional, defaults to now. |
Gotchas
- Passing a key label instead of an ID.
api_key_idsonly accepts the key's ID, not its display name. Fix: list API keys first and read theidfield, or store the ID alongside the label in your own system. - Filtering and grouping on the same field and expecting a filtered, ungrouped result. If you filter to one workspace and also group by
workspace_id, you will get a single-row grouped result, which is harmless but redundant. Fix: only group by a field when you have more than one value of it in scope. - Assuming an empty result means the key never existed. An empty report just as often means the key had zero usage in that window. Fix: check the key's existence separately via the Admin API keys endpoint before concluding it is invalid.
- Forgetting that a deleted or rotated key still has historical usage. Usage data persists after a key is revoked. Fix: keep a local mapping of key ID to label even after rotation, so old reports remain readable.
- Building a huge
api_key_idslist to work around the lack of a "not in workspace X" filter. The API filters by inclusion, not exclusion. Fix: fetch all keys for the org, subtract the ones you want to exclude in Python, then pass the remainder as the filter. - Re-running a per-key report in a loop, once per key. This multiplies API calls unnecessarily. Fix: use
group_by=["api_key_id"]in a single call instead of N separate filtered calls.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Filter by api_key_ids / workspace_ids | You already know exactly which key or workspace you care about | You need to compare more than a handful of identifiers, since grouping scales better |
Group by api_key_id / workspace_id | You want every identifier's usage visible at once, e.g. a leaderboard | You only care about one specific identifier and want the smallest possible response |
| Console Usage and Cost pages, filtered in the UI | A one-off manual lookup, no code needed | You need this on a recurring schedule or joined with other business data |
FAQs
What is the difference between filtering by workspace_ids and grouping by workspace_id?
- Filtering (
workspace_ids=[...]) restricts the report to only the workspaces you list, returning fewer rows. - Grouping (
group_by=["workspace_id"]) keeps all workspaces in scope but breaks the result into one row per workspace. - Use filtering when you know exactly who you're looking for; use grouping when you want to compare many.
Can I filter by both api_key_ids and workspace_ids in the same request?
Yes. Both filters can be applied together, and the API returns usage that matches both conditions. This is useful for narrowing down to a specific key that belongs to a specific workspace when key IDs alone aren't unique enough for your use case.
How do I find an API key's ID if I only know its label in the Console?
import anthropic
client = anthropic.Anthropic()
keys = client.beta.admin.api_keys.list()
match = next(k for k in keys.data if k.name == "backend-prod")
print(match.id)Does filtering by API key also filter cost data, or only usage data?
Both. The same api_key_ids and workspace_ids filters apply to the cost report endpoint as well as the usage report endpoint, since cost is derived from the same underlying records.
What happens if I pass an API key ID that doesn't exist?
The report call succeeds but returns no data for that ID, rather than raising an error. This is worth handling explicitly if you're building automation that should alert on a missing or mistyped key ID.
Can I query usage for a key that has since been revoked?
Yes. Historical usage records are tied to the key ID and persist independently of whether the key is still active, so revoked keys remain queryable for as long as your organization's data retention window covers.
Is there a limit to how many API key IDs I can pass in one filter?
The list accepts multiple IDs, but very large lists are better handled with grouping instead of an enormous filter list. If you're filtering to more than a few dozen keys, switch to group_by=["api_key_id"] and post-filter in Python.
How do I combine a workspace filter with a model breakdown?
report = client.beta.usage.report(
starting_at="2026-06-01T00:00:00Z",
workspace_ids=["wksp_01abc123"],
group_by=["model"],
)This filters to one workspace, then groups the remaining rows by model.
Why would I group by api_key_id instead of just fetching each key's usage separately?
A single grouped call is one network round trip and one query against the underlying data, versus N calls for N keys. It also guarantees every key's data comes from the exact same time window, which matters when you're building a report that needs to add up correctly.
Do workspace IDs and API key IDs use the same format?
No, they use distinct prefixes (for example workspace IDs commonly begin with wksp_ and API key IDs with apikey_), which makes it easy to catch a mixed-up filter value at a glance during code review.
Can I filter by a team or department instead of a workspace or key?
Not directly, the API only understands API key ID and workspace ID as identity filters. If your organization maps teams to workspaces or a set of keys, you build that mapping yourself and apply it in your own code after fetching the grouped report.
Should I filter server-side with the API parameters, or fetch everything and filter in Python?
Filter server-side with api_key_ids or workspace_ids whenever you know the target ahead of time. It reduces the amount of data transferred and the amount of aggregation the API has to do, and it keeps your Python code simpler since it isn't re-implementing filtering logic the API already provides.
Related
- How the Usage & Cost Admin API Models Your Spend - the token-bucket model these filters slice into.
- Usage & Cost Tracking Basics - the unfiltered report this page builds on.
- Building a Cost Dashboard from Uncached, Cached, and Output Token Breakdowns - turn a filtered or grouped report into a dashboard.
- Filtering Usage by Model, Service Tier, and Data Residency - the other dimensions you can slice on beyond key and workspace.
- ADR Template: A Chargeback Model for Multi-Team API Key Usage - how per-key filtering feeds a real chargeback policy.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, pricing, and SDK versions move quickly - verify current specifics at platform.claude.com/docs before relying on them.