Analytics API
Access business metrics and performance data programmatically. Every endpoint accepts either an admin session token (Authorization: Bearer <JWT>) or an admin-role API key in X-API-Key. See Authentication.
Analytics data is pre-aggregated and refreshed every 15 minutes. Requests return immediately from the stored figures and never compute on the spot. To get fresh figures sooner, recompute now.
Dashboard Overview
GET /api/v1/analytics/overview
Returns the headline figures, the cost breakdown by service, and the invoice pipeline.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
period | string | Rolling window like 7d, 30d, 90d, from 1 to 1095 days (default: 30d). Ignored when from_date/to_date are supplied. |
from_date | string | Custom range start (YYYY-MM-DD, inclusive). Supply with to_date; together they override period. |
to_date | string | Custom range end (YYYY-MM-DD, inclusive). Supply with from_date. |
X-Environment header | string | live or sandbox (header — defaults to live; not a query parameter) |
A partial pair (only one of from_date/to_date), a non-YYYY-MM-DD value, or from_date > to_date
returns 422. So does a period outside its range — a window of zero, a negative one, or a count
past the ceiling — on every analytics endpoint that takes one.
This endpoint reports a rolling window (the period, or your custom from_date/to_date range).
The Product Performance endpoint (/analytics/products) instead reports per calendar month — its
period sizes only the customer-growth trend, while its other fields are point-in-time / latest-month /
last-30-days. So a "new customers" figure here (e.g. last 30 days) is not expected to equal the current
month's bar there. Pick the endpoint whose window matches what you're measuring.
Response:
{
"period": {"start": "2026-03-17", "end": "2026-04-15"},
"revenue": {
"total": 12500.00,
"subscriptions": 8000.00,
"usage_revenue": 4500.00,
"change_pct": 15.2
},
"customers": {
"active": 145,
"paid": 120,
"free": 25,
"net_growth": 9,
"new": 12,
"churned": 3,
"change_pct": 6.6
},
"invoice_pipeline": {
"total_amount": 15000.00,
"total_count": 40,
"collected_amount": 11000.00,
"collected_count": 30,
"outstanding_amount": 4000.00,
"outstanding_count": 10,
"aging": {
"current": 2500.00, "current_count": 6,
"days_1_30": 1000.00, "days_1_30_count": 3,
"days_31_60": 500.00, "days_31_60_count": 1,
"days_61_90": 0.00, "days_61_90_count": 0,
"days_90_plus": 0.00, "days_90_plus_count": 0,
"open_count": 10, "overdue_count": 4
}
},
"costs": {
"total": 3200.00,
"change_pct": 0.0,
"by_service": [
{"service_id": "4", "name": "Subscriptions", "cost": 0.00, "revenue": 8000.00, "pct": 0.0, "margin_pct": 100.0, "cost_not_stored": false},
{"service_id": "181", "name": "ai-input-tokens", "cost": 2800.00, "revenue": 3500.00, "pct": 87.5, "margin_pct": 20.0, "cost_not_stored": false},
{"service_id": "255", "name": "api-calls", "cost": 400.00, "revenue": 1000.00, "pct": 12.5, "margin_pct": 60.0, "cost_not_stored": false}
]
},
"gross_profit": {"amount": 9300.00, "margin_pct": 74.4, "change_pct": 12.8},
"computed_at": "2026-04-15T10:30:00Z"
}
Field notes:
revenue.subscriptions— net subscription charges (can be negative the day after a plan change, when the prorated refund posts)revenue.usage_revenue: net usage chargescosts.by_service[].revenue— revenue attributed to the service, not necessarily what it billed. Where one charge and several vendor costs belong to a single transaction, that transaction's revenue is split across the services that carried its cost, so every part of the transaction reports the transaction's own margin. Revenue billed outside a transaction keeps its own service, which is whySubscriptionsstill appears above; a service whose revenue was attributed away drops out of the list.costs.by_service[].margin_pct—(revenue - cost) / revenue, over that attributed revenue. Usage covered by an included quota still reports-100: nothing links it to the subscription that paid for it. It isnullwhencost_not_storedis set, because a margin over an incomplete cost overstates the margin.costs.by_service[].cost_not_stored—truewhen a day in the period holds a vendor cost the aggregator never stored. The cost shown understates and no margin is reported. Aggregate the period again to restore the figure; the scheduled run already covers the last 30 days.costs.change_pct: currently0.0.computed_at: when the figures were last computed, in UTC.nulluntil the first run.
Revenue Trend
GET /api/v1/analytics/revenue/trend
Daily revenue breakdown by type — one point per day in the selected window. This endpoint is historical-only; no forward projection is included. (A separate monthly endpoint covers projections.)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
period | string | Rolling window like 7d, 30d, 90d, from 1 to 1095 days (default: 30d). Ignored when from_date/to_date are supplied. |
granularity | string | daily (default). |
from_date | string | Custom range start (YYYY-MM-DD). Supply with to_date; overrides period. |
to_date | string | Custom range end (YYYY-MM-DD). Supply with from_date. |
Response:
{
"period": {"start": "2026-03-17", "end": "2026-04-15"},
"data": [
{
"date": "2026-04-01",
"revenue_subscriptions": 280.00,
"revenue_per_usage": 145.00
},
{
"date": "2026-04-02",
"revenue_subscriptions": 280.00,
"revenue_per_usage": 162.50
}
]
}
Field notes:
revenue_subscriptions: net subscription charges for that day. It can be negative on the day after a plan change, when the prorated refund posts.
Cost Trend
GET /api/v1/analytics/costs/trend
Daily cost breakdown by service.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
period | string | Rolling window like 7d, 30d, 90d, from 1 to 1095 days (default: 30d). Ignored when from_date/to_date are supplied. |
granularity | string | daily (default). |
from_date | string | Custom range start (YYYY-MM-DD). Supply with to_date; overrides period. |
to_date | string | Custom range end (YYYY-MM-DD). Supply with from_date. |
X-Environment header | string | live or sandbox (header, not a query parameter) |
Response:
{
"period": {
"start": "2026-04-01",
"end": "2026-04-02"
},
"services": ["181", "255"],
"service_names": {
"181": "ElevenLabs API",
"255": "GPU Compute"
},
"data": [
{
"date": "2026-04-01",
"181": 90.00,
"255": 15.00
},
{
"date": "2026-04-02",
"181": 84.50,
"255": null
}
]
}
A per-service cost of null and one of 0 mean different things. 0 is a service
that genuinely cost nothing that day. null is a figure the aggregator never
stored, which happens for a day computed by an older build that kept only two
decimals and so lost a real sub-cent cost. Aggregate that period again to restore
it; the scheduled run already covers the last 30 days. The dashboard shows a
null as "Not stored" rather than as $0.00.
Revenue Detail
GET /api/v1/analytics/revenue
MRR breakdown, top customers, and per-plan revenue.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
period | string | 3m, 6m, 9m, 12m, from 1 to 60 months (default: 9m) |
service_id | string | Optional. Narrows top_customers to one service's own usage-ranked customers instead of the overall revenue ranking. Use one of the ids in service_usage_options. |
X-Environment header | string | live or sandbox (header, not a query parameter) |
Response:
{
"mrr": {
"current": 9500.00,
"new": 650.00,
"churned": -150.00,
"change_pct": 5.6
},
"usage_revenue": {
"current": 2400.00,
"change_pct": 8.1
},
"arr_projected": 114000.00,
"gross_profit": {
"amount": 9000.00,
"margin_pct": 75.6
},
"by_plan": [
{
"plan_id": "42",
"name": "Pro",
"customers": 48,
"mrr": 4800.00,
"usage_revenue": 1500.00,
"revenue_pct": 52.9
},
{
"plan_id": "43",
"name": "Starter",
"customers": 94,
"mrr": 4700.00,
"usage_revenue": 900.00,
"revenue_pct": 47.1
}
],
"top_customers": [
{"customer_id": "870", "name": "Acme Corp", "mrr": 100.00, "usage_revenue": 300.00, "total": 400.00, "margin_pct": 78.2, "cost": 87.20},
{"customer_id": "912", "name": "Beta Labs", "mrr": 100.00, "usage_revenue": 240.00, "total": 340.00, "margin_pct": 73.0, "cost": 91.80},
{"customer_id": "1004", "name": "Cobalt AI", "mrr": 100.00, "usage_revenue": 190.00, "total": 290.00, "margin_pct": 75.0, "cost": 72.50},
{"customer_id": "1033", "name": "Delta Voice", "mrr": 100.00, "usage_revenue": 150.00, "total": 250.00, "margin_pct": 72.0, "cost": 70.00},
{"customer_id": "1051", "name": "Echo Systems", "mrr": 50.00, "usage_revenue": 180.00, "total": 230.00, "margin_pct": 76.0, "cost": 55.20},
{"customer_id": "1078", "name": "Fern Studio", "mrr": 100.00, "usage_revenue": 110.00, "total": 210.00, "margin_pct": 76.0, "cost": 50.40},
{"customer_id": "1102", "name": "Granite Labs", "mrr": 50.00, "usage_revenue": 140.00, "total": 190.00, "margin_pct": 74.0, "cost": 49.40}
],
"top_customers_others": {
"customer_count": 135,
"mrr": 8900.00,
"usage_revenue": 1090.00,
"total": 9990.00,
"cost": 2423.50,
"margin_pct": 75.7
},
"service_usage_options": [
{ "service_id": "181", "service_name": "ElevenLabs API" }
],
"top_customers_by_service": null,
"trend": [
{ "month": "2026-03", "mrr": 9000.00, "usage_revenue": 2220.00, "actual": true },
{ "month": "2026-04", "mrr": 9500.00, "usage_revenue": 2400.00, "actual": true },
{ "month": "2026-05", "mrr": 10000.00, "usage_revenue": 2580.00, "actual": false },
{ "month": "2026-06", "mrr": 10500.00, "usage_revenue": 2760.00, "actual": false },
{ "month": "2026-07", "mrr": 11000.00, "usage_revenue": 2940.00, "actual": false }
]
}
top_customers_others aggregates every customer beyond the individually-listed top_customers (the
top customers by revenue) — sum top_customers and top_customers_others together to get the tenant's
full revenue for the period. Revenue is net of refunds, and only a customer with a positive total for
the month is listed in top_customers: one at zero or below is summed here instead. customer_count
is 0 and every amount is 0 when every customer billed in the month is listed.
In by_plan, customers and mrr count the plan's active subscribers, so the rows add up to
mrr.current. usage_revenue and revenue_pct are what the month billed.
service_usage_options always lists the services with usage data this month, whether or not you pass
service_id. Use it to populate a service picker. It's empty if no service has any usage yet.
top_customers_by_service is null unless you pass service_id, in which case it replaces null
with that one service's own top-seven-by-usage ranking:
{
"top_customers_by_service": {
"service_id": "181",
"service_name": "ElevenLabs API",
"unit": "characters",
"customers": [
{ "customer_id": "870", "name": "Acme Corp", "usage": 4200000, "revenue": 630.00 }
],
"others": { "customer_count": 12, "usage": 9800000, "revenue": 1450.00 }
}
}
Ranking is by usage, not revenue, so a heavy free-tier consumer can outrank a light-usage paying
customer here even though they wouldn't in top_customers. There's no margin or cost figure in this
view. An id with no matching service still gets a 200 with an empty customers list, never a
404: the same "answer with zeros rather than refuse" rule every analytics endpoint follows.
Product Performance
GET /api/v1/analytics/products
Per-plan revenue, customer counts, churn, and a customer-growth trend.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
period | string | Number of calendar months in the growth trend, from 1 to 60, e.g. 3m, 6m, 12m (default: 6m). |
X-Environment header | string | live or sandbox (header, not a query parameter) |
period sizes only the customer_growth trend (one point per calendar month). Every other field
ignores it: customer_growth is per calendar month; free_users.active_30d is
last-30-days; churn_rate, new_customers_by_plan, profit_by_plan, quota_utilization are the
latest calendar month; the rest are point-in-time. To compare against the rolling
/analytics/overview, match the windows yourself — the two endpoints are not meant to return identical
period totals.
Response:
{
"free_users": {
"total": 120,
"active_30d": 45,
"change": 20
},
"trial_users": {
"total": 14,
"expiring_7d": 3,
"avg_days_remaining": 6.5
},
"churn_rate": {
"value": 2.1,
"target": 5.0,
"status": "ok"
},
"customer_growth": [
{ "month": "2026-03", "new": 18, "churned": 3 },
{ "month": "2026-04", "new": 22, "churned": 2 }
],
"quota_utilization": [
{
"plan_name": "Pro",
"service_id": "181",
"service_name": "ElevenLabs API",
"customers_total": 48,
"under": 30,
"near": 12,
"over": 6
}
],
"new_customers_by_plan": [
{ "plan_name": "Pro", "count": 9 },
{ "plan_name": "Starter", "count": 13 }
],
"profit_by_plan": [
{ "plan_name": "Pro", "profit": 4620.00, "margin_pct": 73.3, "revenue": 6300.00, "cost": 1680.00 },
{ "plan_name": "Starter", "profit": 4380.00, "margin_pct": 78.2, "revenue": 5600.00, "cost": 1220.00 }
]
}
Plan Analytics
GET /api/v1/analytics/plans/{plan_id}
Detailed KPIs and charts for a single plan.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
plan_id | string | The plan's numeric ID (visible in the Plans list) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
X-Environment header | string | live or sandbox (header, not a query parameter) |
Response:
{
"period": {
"start": "2026-04-01",
"end": "2026-04-30"
},
"scope": "plan",
"customer_id": null,
"attributed_plan_id": null,
"attributed_plan_name": null,
"computed_at": "2026-04-30T06:15:00Z",
"kpi": {
"run_rate": {
"last_30d": 5600.00,
"last_24h": 186.67,
"change_30d_pct": 0.0,
"change_24h_pct": 0.0
},
"subscribers": {
"current": 48
},
"event_count": {
"last_30d": 12400,
"last_24h": 413,
"change_30d_pct": 0.0,
"change_24h_pct": 0.0
}
},
"service_usage_30d": [
{
"service_id": "181",
"service_name": "ai-input-tokens",
"trend": [
{"date": "2026-04-15", "actual": 48200.0, "allocated": 0.0}
]
}
],
"revenue_margin_4w": [
{"week_label": "Week 1", "revenue": 1280.00, "margin": 960.00},
{"week_label": "Week 2", "revenue": 1350.00, "margin": 1012.00},
{"week_label": "Week 3", "revenue": 1290.00, "margin": 967.00},
{"week_label": "Week 4", "revenue": 1310.00, "margin": 982.00}
]
}
Field notes:
kpi.run_rate— total revenue generated by customers on this plan in the windowkpi.subscribers.current— active customers on this plan right nowkpi.event_count: number of usage charges attributed to this plan. A subscription fee counts towardkpi.run_rate, not here.service_usage_30d: one series per service the plan includes with non-zero usage quantity, ordered by service name, then id. Usage of a service the plan no longer includes, or that a customer used on an earlier plan, is left out, while its charges stay inkpi.run_rate. Each daily point'sactualis the measured quantity (e.g. token count). The per-dayallocatedis unused (always0). Quota utilization is not shown here; see the dashboard "Quota Utilization by Plan" card (a per-customer under/near/over distribution).revenue_margin_4w— last 4 ISO weeks; fewer entries if the plan has less than 4 weeks of datachange_*_pctfields are currently0.0(v1 limitation — requires a previous-window snapshot)
Recompute Now
POST /api/v1/analytics/refresh
Asks for your analytics to be recomputed now, rather than at the next scheduled run. It takes no body and returns as soon as the work is queued.
Response:
{ "triggered": true, "detail": "Recompute started." }
| Field | Description |
|---|---|
triggered | true when a recompute started. false when there is nothing to recompute: demo data, or a deployment where on-demand recompute is not configured. |
detail | Readable explanation of the outcome. |
After a true, poll computed_at on Dashboard Overview every few seconds until it
advances, and stop after about 90 seconds: a recompute that has not finished by then may have failed,
and the figures you have are the latest. After a false, the figures you already have are the
current ones.