How to Build a Claude API Usage Analytics Dashboard
If you're integrating Claude into a product, you eventually hit the same wall: you can see your total bill at the end of the month, but you have no idea which feature, customer, or team is driving it. Building a usage analytics dashboard solves that — it gives you token counts, cost, latency, and error rates broken down by whatever dimension matters to your business, in near real time instead of 30 days late.
This article walks through what data to capture, how to structure the pipeline, and what to visualize, so you can build this yourself on top of the raw Claude API, or skip most of the plumbing if you're already using a layer like SubToAPI that exposes this metadata per call.
What "usage analytics" actually means for an LLM API
Unlike a typical REST API, cost and performance for Claude aren't fixed per request — they scale with input and output tokens, which vary wildly by prompt, context length, and tool use. A useful dashboard needs to answer:
- Cost per unit of work — per user, per customer account, per feature/endpoint, per model
- Token breakdown — input vs output vs cached tokens (cache hits change pricing significantly)
- Latency — time to first token and total completion time, especially if you stream
- Error and retry rates — rate limits, timeouts, malformed tool calls
- Volume trends — requests per day/hour, to catch spikes or abuse early
Without this, teams typically discover cost problems only when finance flags an invoice. A dashboard turns that into a daily or weekly check.
Step 1: Capture usage data at the source
Every Claude API response includes a usage object with input and output token counts. That's your raw material — you need to persist it, not just read it and discard it.
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.CLAUDE_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json"
},
body: JSON.stringify({
model: "claude-opus-4",
max_tokens: 1024,
messages: [{ role: "user", content: "Summarize this report." }]
})
});
const data = await response.json();
await logUsageEvent({
model: data.model,
input_tokens: data.usage.input_tokens,
output_tokens: data.usage.output_tokens,
user_id: currentUser.id,
feature: "report-summary",
latency_ms: Date.now() - startTime,
status: response.status
});
The key design decision here is what dimensions you tag each event with. At minimum: user/account ID, feature or endpoint name, model, and timestamp. If you serve multiple customers or teams, tag the event with that too — it's much harder to retrofit attribution later than to add one field now.
If you're calling Claude through SubToAPI instead of directly, this step is largely done for you: every request made with a sub_live_... key is already logged with usage metadata, model, latency, and status, viewable per application key in the dashboard — see /docs/messages for the response shape.
Step 2: Store events in a queryable format
A simple append-only table works fine for most teams before you need a dedicated time-series database:
CREATE TABLE claude_usage_events (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT now(),
model TEXT,
input_tokens INT,
output_tokens INT,
cache_read_tokens INT DEFAULT 0,
user_id TEXT,
team_id TEXT,
feature TEXT,
latency_ms INT,
status INT
);
Index created_at, team_id, and feature — those are the columns you'll filter and group by constantly. Cost isn't stored directly; compute it at query time from current per-model pricing so you can re-run historical reports if pricing changes.
Step 3: Aggregate for the views you actually need
Most dashboards need three views, and each is a different query shape:
Daily cost trend
SELECT
date_trunc('day', created_at) AS day,
model,
SUM(input_tokens) AS input_tokens,
SUM(output_tokens) AS output_tokens
FROM claude_usage_events
GROUP BY 1, 2
ORDER BY 1 DESC;
Per-team/per-feature breakdown
SELECT team_id, feature, COUNT(*) AS requests,
SUM(output_tokens) AS output_tokens
FROM claude_usage_events
WHERE created_at > now() - interval '30 days'
GROUP BY team_id, feature
ORDER BY output_tokens DESC;
Error rate
SELECT date_trunc('hour', created_at) AS hour,
AVG(CASE WHEN status >= 400 THEN 1 ELSE 0 END) AS error_rate
FROM claude_usage_events
GROUP BY 1 ORDER BY 1 DESC LIMIT 48;
Run these on a schedule into a materialized view or a small summary table — querying the raw event table live for every dashboard load doesn't scale once you're past a few hundred thousand rows.
Step 4: Visualize and set alerts
For the frontend, you don't need anything exotic — a line chart for daily cost, a stacked bar for per-feature token usage, and a table for top consumers covers 90% of use cases. Tools like Grafana, Metabase, or a simple React charting library all work; the hard part was never the chart, it was getting clean, attributed data into a table.
Add threshold alerts early: a daily cost spike, a sudden jump in error rate, or one user/team suddenly consuming 10x their normal volume are the three alerts that catch real problems before they become invoices.
If you'd rather not build and maintain this pipeline, SubToAPI ships per-key usage tracking, request logs, and team-level breakdowns out of the box, so the analytics layer is already there once you route Claude traffic through it. Streaming requests are tracked the same way — see /docs/streaming for details on event shapes during a stream.
Keep it simple at first
Resist building a generic "LLM observability platform" on day one. Start with: log every call with usage, tag it with who/what/when, aggregate daily, chart it. Add cache token tracking, latency percentiles, and per-model cost comparisons once you have a few weeks of real data and know which questions people actually ask about the dashboard.
Questions
Do I need a separate database for usage analytics, or can I reuse my app's main database? For most teams under a few million events, a table in your existing Postgres database with proper indexes is fine. Move to a dedicated analytics store (ClickHouse, BigQuery) only once query latency on the main database becomes a problem.
How do I track cost accurately when Claude pricing changes? Store raw token counts, not computed cost, and apply pricing at query time from a small lookup table keyed by model and effective date. That lets you recompute historical costs correctly after a price change.
Can I get usage analytics without building this myself? Yes — if you route requests through SubToAPI, usage per key, model, and request is already tracked and visible in the dashboard. Check /pricing for plan details or start a trial at /signup.