●CONNECTORS — Managed MCP connectors gain connector observability in public beta for adoption, errors, latency, and usage●DIRECTORY — Admins can now submit MCP connectors to the directory directly from Claude●SLACK — Team and Enterprise users can tag Claude in Slack to delegate tasks●ENTERPRISE — Claude Enterprise adds richer admin analytics, model-level entitlements, and spend alerts●MODEL — Claude Sonnet 5 is the default across all plans at $2/$10 per million tokens through August 31●LIMITS — Claude Code weekly usage limits are up 50% through July 13; Claude Science applications close July 15●CONNECTORS — Managed MCP connectors gain connector observability in public beta for adoption, errors, latency, and usage●DIRECTORY — Admins can now submit MCP connectors to the directory directly from Claude●SLACK — Team and Enterprise users can tag Claude in Slack to delegate tasks●ENTERPRISE — Claude Enterprise adds richer admin analytics, model-level entitlements, and spend alerts●MODEL — Claude Sonnet 5 is the default across all plans at $2/$10 per million tokens through August 31●LIMITS — Claude Code weekly usage limits are up 50% through July 13; Claude Science applications close July 15
Four sites, one Claude bill: attributing spend per workspace to decide which pipeline to trim
When several projects share one Claude organization, the bill arrives as a single number that hides which pipeline is expensive. Field notes on splitting that spend per workspace with the Cost and Usage Report group_by, defensively parsing the results, and deciding what to trim by cost-effectiveness.
One morning at the start of the month I opened Claude's cost page and paused. Spend was clearly up from the previous month. But whether that increase came from claudelab, from gemilab's overnight batch, or from somewhere else — I had no way to tell.
As an indie developer, I run four technical blogs on my own, and I route their article generation, summarization, and link-checking through a single Claude organization. Convenient, but the cost comes back as one merged bill. I could see the total hovering around $4 a day; I could not see which site's which job was eating it. And without that, there is no way to know where a cut would actually help.
This article walks through splitting the Cost Report and Usage Report by workspace so a merged bill can be attributed back to each site. The basics of the two endpoints live in "Monitoring Claude API cost programmatically with the Usage & Cost API"; here I focus one level deeper, on the allocation — the showback.
Why a merged bill dulls your judgment
A single number hides problems inside its average.
At four sites totaling $4 a day, the intuitive read is "about $1 per site." When I actually divided it, one site accounted for roughly 48% of the total and the other three split the remaining 52%. The growth lived in one specific pipeline. Had I kept staring at the total, I would have chased a pointless optimization — shaving a little off everything, evenly and uselessly.
There is a second trap: the imbalance in cache creation tokens. One site rewrites the front of its prompt so often that caching barely helps, and cache creation made up a large share of its input tokens. That is completely buried in a merged view. Where a fix will pay off only becomes visible after you divide.
A self-maintained token ledger (the approach in "accounting the four usage buckets correctly") can allocate too, but it drifts quietly from the invoice through missed records and mishandled failed requests. Starting from the authoritative Cost Report gives you a correct total to build on first.
Two axes for splitting spend: workspace and API key
Anthropic's administrative endpoints offer two ways to slice spend.
Axis
Cost Report
Usage Report
Where it fits for solo ops
Workspace
Yes
Yes
One workspace per site. Clean boundary — my recommendation
API key
No
Yes
If you run one key per site. Note: revoked keys still appear as rows
Model
Effectively one
Yes
See the Opus 4.8 / Sonnet 5 / Haiku split to find downgrade room
Service tier
Yes
Yes
Understand the price gap between batch and standard
I settled on one workspace per site. Keys are easy, but rotating a key gives its past and present rows different IDs and breaks the continuity of your aggregation. Workspaces are long-lived, and once you build a workspace_id → site name map it keeps serving you.
Note that the Cost Report is daily (1d) granularity only. When you want a finer waveform, pull the Usage Report at bucket_width=1h and multiply by unit prices to approximate. In practice: the Cost Report for the exact billed amount, the Usage Report for resolution on the breakdown.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Pulling Cost Report and Usage Report with group_by=workspace to split one org bill back into per-site spend
✦Defensive parsing that survives changing fields, plus a day-by-site showback pivot you can read at a glance
✦A cost-per-click and cache-creation-ratio lens for choosing which pipeline to actually trim
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Prerequisites: workspace separation and an Admin key
Allocation depends on spend being recorded separately from the start. You cannot re-divide it after the fact. My setup:
Create a workspace per site in the Console (ws-claudelab, ws-gemilab, …)
Each site's automation uses only the API key bound to its workspace
Organization-wide aggregation uses an org-scoped Admin API key (the sk-ant-admin… form)
You issue the Admin API key from the organization settings in the Console. Keep it clearly separate from your workspace keys (the execution keys each site uses), or you will fail silently with a 401 or an empty response.
# For org aggregation (used by the allocation-report script)export ANTHROPIC_ADMIN_KEY="YOUR_ADMIN_API_KEY"# A different thing from each site's execution key. Do not mix them.export ANTHROPIC_WORKSPACE_KEY="YOUR_WORKSPACE_API_KEY"
The Admin API is a feature for Console accounts that have an organization. A standalone claude.ai consumer account cannot use it, so this assumes you use the API as an organization.
Get moving: fetch the Cost Report per workspace
The smallest first step is to pull the last seven days of cost per workspace. The endpoint is GET /v1/organizations/cost_report. Add group_by[]=workspace_id and pass starting_at in ISO 8601.
The important part is to not hard-code the response fields. Administrative endpoints gain items over time, and it is easy to miss that amount comes back as a string. Access dicts with get and a fallback, and always receive money as Decimal.
import osimport httpxfrom decimal import Decimalfrom datetime import datetime, timedelta, timezoneBASE = "https://api.anthropic.com"HEADERS = { "x-api-key": os.environ["ANTHROPIC_ADMIN_KEY"], "anthropic-version": "2023-06-01",}def fetch_cost_report(days: int = 7) -> list[dict]: """Fetch `days` days of Cost Report grouped by workspace_id, all pages.""" start = (datetime.now(timezone.utc) - timedelta(days=days)).date().isoformat() params = { "starting_at": f"{start}T00:00:00Z", "group_by[]": "workspace_id", "limit": 31, } buckets: list[dict] = [] with httpx.Client(timeout=30.0) as client: page = None while True: q = dict(params) if page: q["page"] = page r = client.get(f"{BASE}/v1/organizations/cost_report", headers=HEADERS, params=q) r.raise_for_status() body = r.json() buckets.extend(body.get("data", [])) if not body.get("has_more"): break page = body.get("next_page") return buckets
I page from the start because days × workspaces grows past what one response holds. Follow next_page until has_more is false. Skip this and you can silently drop a day, produce a plausible-looking total, and later agonize over why it does not match the invoice.
Build a day-by-site showback table
Fold the buckets into a date × site name pivot. workspace_id is opaque, so translate it to a human-readable site name through a map you maintain. Keep IDs that are not in the map instead of dropping them — as unknown. That is exactly what later tips you off that you forgot to register a newly created workspace.
from collections import defaultdictWORKSPACE_TO_SITE = { "wrkspc_claudelab": "claudelab", "wrkspc_gemilab": "gemilab", "wrkspc_antigrav": "antigravitylab", "wrkspc_rorklab": "rorklab",}def build_showback(buckets: list[dict]) -> dict[str, dict[str, Decimal]]: """Return {date: {site: amount}}. Unknown workspaces collapse to 'unknown'.""" table: dict[str, dict[str, Decimal]] = defaultdict(lambda: defaultdict(Decimal)) for b in buckets: day = (b.get("starting_at") or "")[:10] for row in b.get("results", []): ws = row.get("workspace_id") or "default" site = WORKSPACE_TO_SITE.get(ws, "unknown") amount = Decimal(str(row.get("amount", "0"))) table[day][site] += amount return tabledef print_showback(table: dict[str, dict[str, Decimal]]) -> None: sites = sorted({s for day in table.values() for s in day}) header = "date".ljust(12) + "".join(s.rjust(14) for s in sites) + "total".rjust(14) print(header) grand = defaultdict(Decimal) for day in sorted(table): row = table[day] line = day.ljust(12) day_total = Decimal("0") for s in sites: v = row.get(s, Decimal("0")) day_total += v grand[s] += v line += f"${v:.3f}".rjust(14) line += f"${day_total:.3f}".rjust(14) print(line) total_all = sum(grand.values()) foot = "TOTAL".ljust(12) + "".join(f"${grand[s]:.2f}".rjust(14) for s in sites) foot += f"${total_all:.2f}".rjust(14) print("-" * len(header)); print(foot) for s in sites: share = (grand[s] / total_all * 100) if total_all else Decimal("0") print(f" {s:<16} {share:5.1f}%")
Receiving amount as Decimal(str(...)) matters because turning money into a float lets rounding error pile up and spoils authoritative data. Keep totals in Decimal all the way through.
Before / After: one merged line vs. the split breakdown
Here are the same seven days seen two ways.
Before (when I only watched the total)
7-day total: $28.10 (about $4.01 / day)
-> I implicitly assumed an even ~$1 per site
After (divided back out by workspace)
date claudelab gemilab antigravitylab rorklab total
2026-07-01 $1.902 $0.611 $0.402 $0.281 $3.196
...
TOTAL $13.480 $6.240 $4.520 $3.860 $28.100
claudelab 48.0%
gemilab 22.2%
antigravitylab 16.1%
rorklab 13.7%
The even-split assumption was wrong. claudelab was about 48% of everything. And when I pulled the Usage Report with both group_by[]=model and group_by[]=workspace_id, the breakdown of that 48% showed cache creation tokens at roughly 30% of input. The front of the prompt changed too much between articles, so caching wasn't landing — the thing to trim was not "all site usage" but that single point. How to measure cache hit rate is in my prompt-cache field notes.
Decide the trim by numbers: a cost-effectiveness lens
Once you can allocate, the next question is which pipeline to cut. Deciding by raw dollars risks cutting the job that produces the most value. I divide by each site's outcome and compare. This lens is a habit carried over from watching AdMob revenue per install on my indie apps.
def cost_efficiency(grand: dict[str, Decimal], clicks_7d: dict[str, int]) -> None: """Cost per 100 clicks per site. Lower is more efficient.""" print(f"{'site':<16}{'cost/7d':>10}{'clicks/7d':>12}{'cost/100clk':>14}") for site in sorted(grand): cost = grand[site] clicks = clicks_7d.get(site, 0) per_100 = (cost / clicks * 100) if clicks else Decimal("0") print(f"{site:<16}${cost:>8.2f}{clicks:>12}${per_100:>12.3f}")
Even the site with the largest absolute cost, claudelab, is a well-placed investment if its cost per 100 clicks is the lowest. Conversely, a site whose dollars are small but whose cost per click stands out is the first candidate for revisiting cache design or model choice (a downgrade to Sonnet 5). Keeping the unit prices from drifting when a new model appears is something I wrote up in a pricing registry with fail-closed design.
You could wire a notification when a threshold is crossed, but ringing too often makes it harder, not easier, to notice. The design of how to ring belongs to another article. The focus here is only this: divide correctly, then compare correctly.
Where I stumbled
A few things caught me while putting allocation into practice.
Records from any period when workspaces were not separated collapse into the default workspace, and cannot be re-divided afterward. Allocation is a design for going forward, not something you can apply retroactively — accept that up front.
Grouping by api_key_id returns rows for keys you have already revoked, as history. Steer unknown IDs to unknown here too so the aggregation script does not crash on an "unfamiliar key."
The Cost Report is 1d granularity only. An overnight batch that crosses the UTC day boundary appears split across two days. I fixed my aggregation to the UTC calendar day and read it knowing it diverges from how JST feels. Timezone accidents in log dates are an easy trap even for a solo operator.
And the money. amount returns as a string. Receive it with float() by accident and, across seven days and four sites, the trailing digits slowly go wrong. Keep Decimal all the way through.
Closing
That one merged number was a kind of gentle lie. It let me feel like I could see the whole, while actually hiding where a fix belonged. Only after dividing it back out by workspace did the source of the growth resolve to a single point, and did the place to act become clear.
If you run several projects inside one Claude organization, start by separating them into workspaces. Simply being able to divide turns the cost conversation from "somehow it's high" into "fix this here." I am still in the middle of tuning my own setup, but I hope this helps anyone running things the same way. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.