Usage & Balance API
Workspace REST endpoints for reading your own balances, usage history, transactions, analytics, and exporting data — via a browser session or a zr_ API key.
These endpoints expose read-only access to your own workspace data: balances, usage history, transactions, analytics, and CSV/JSON exports. In a browser you call them with your dashboard session; from a server or script you can now use the same zr_ API key you already use for the inference API.
Authentication#
Every endpoint below accepts either credential, whichever arrives first:
- Session cookie — the httpOnly
zrouter_sessioncookie issued by Google sign-in. This is how the dashboard calls the API. - API key — a
zr_key created in the dashboard under API Keys (shown once at creation). Send it in theAuthorizationheader as a Bearer token or in theX-Api-Keyheader.
API key access is limited to these five read-only paths: GET /api/customer/balances, GET /api/customer/usage, GET /api/customer/transactions, GET /api/customer/analytics, and GET /api/customer/exports/{kind}. Everything else under /api/customer/* — checkout, receipt downloads, session management, and API key management — still requires the browser session. If both headers are sent and they carry different keys, the request is rejected.
# Style 1: Authorization Bearer
curl -H "Authorization: Bearer zr_<public-id>_<secret>" \
https://api.zrouter.dev/api/customer/balances
# Style 2: X-Api-Key
curl -H "X-Api-Key: zr_<public-id>_<secret>" \
https://api.zrouter.dev/api/customer/balancesIf a request carries a valid session cookie, the session wins and the API key is ignored. A blocked key returns the same 401 invalid_api_key as a wrong, revoked, or expired key — the responses are indistinguishable by design, so probing which failure mode occurred is not possible.
Rate limits#
Requests authenticated with an API key are limited to 60 requests per minute per key (the default; the operator can change it via CUSTOMER_KEY_RATE_LIMIT_PER_MINUTE). Exceeding the limit returns 429 rate_limited with a Retry-After: 60 header. Repeated attempts with an invalid key are additionally throttled per source IP. Session-cookie calls are not counted against the key limiter.
Errors#
| HTTP | code | When |
|---|---|---|
| 401 | invalid_session | No or invalid session cookie and no API key provided. |
| 401 | invalid_api_key | The API key is wrong, revoked, expired, or blocked — identical body in all cases. |
| 429 | rate_limited | Too many requests. Please retry shortly. See Retry-After. |
| 503 | blacklist_unavailable | Temporary server-side condition. Retry shortly; do not treat the key as invalid. |
List balances#
/api/customer/balancesReturn your PAYG balance (as a decimal string) and the remaining-token summary for every Token Plan you hold.
Responses
{
"payg": { "currency": "USD", "balance": "12.345678" },
"token_plans": [
{
"model": "<public-name>",
"purchased_tokens": 1000,
"used_tokens": 320,
"remaining_tokens": 680,
"status": "active"
}
],
"as_of": "2026-09-16T00:00:00Z"
}{ "error": { "code": "invalid_api_key", "message": "Invalid API key." } }curl -H "Authorization: Bearer zr_<public-id>_<secret>" \
https://api.zrouter.dev/api/customer/balancesList usage#
/api/customer/usageReturn your inference usage events, newest first, with cursor pagination and a 90-day retention window.
Parameters
Page size, 1-100. Default 50.
Opaque cursor from next_cursor / prev_cursor.
Set to true to page backwards. Requires cursor.
RFC3339 lower bound, inclusive.
RFC3339 upper bound, inclusive.
Filter by model public name.
Filter by API key id (UUID). You can only see your own keys.
Responses
{
"data": [
{
"id": "b8c1...",
"request_id": "req_9f2e...",
"model": "<public-name>",
"input_tokens": 1204,
"output_tokens": 356,
"total_tokens": 1560,
"display_cost_usd_micro": "0.000120",
"funding_mode": "token_plan",
"status": "success",
"timestamp": "2026-09-16T04:12:33Z"
}
],
"next_cursor": "eyJvZmZzZXQiOjUwfQ",
"prev_cursor": null,
"total": 1234,
"retention_days": 90
}{ "error": { "code": "invalid_request", "message": "limit must be between 1 and 100" } }{ "error": { "code": "invalid_api_key", "message": "Invalid API key." } }Each item also carries api_key_id, api_key_name, api_key_prefix, endpoint_kind, streamed, cache_read_input_tokens, plan_debit_tokens, payg_debit_usd_micro, funding, settlement_status, completed_at, and settled_at where applicable.
curl -H "X-Api-Key: zr_<public-id>_<secret>" \
"https://api.zrouter.dev/api/customer/usage?limit=100&cursor=eyJvZmZzZXQiOjB9"List transactions#
/api/customer/transactionsReturn your top-up and purchase transactions (checkout orders), newest first, with cursor pagination.
Parameters
Page size, 1-100. Default 50.
Opaque cursor from next_cursor / prev_cursor.
Set to true (or 1) to page backwards. Requires cursor.
Responses
{
"data": [
{
"id": "3f1c...",
"type": "token_plan",
"status": "credited",
"amount_idr": 16000000,
"usd_value_micro": 1000000,
"fee_idr": 800000,
"total_idr": 16800000,
"granted_tokens": 1000,
"created_at": "2026-09-12T08:30:00Z",
"credited_at": "2026-09-12T08:31:02Z"
}
],
"next_cursor": null,
"prev_cursor": null,
"total": 7
}{ "error": { "code": "invalid_request", "message": "invalid cursor" } }{ "error": { "code": "invalid_api_key", "message": "Invalid API key." } }amount_idr, fee_idr, and total_idr are IDR integers (IDR has no minor unit). usd_value_micro is micro-USD. credited_at is set once the payment is credited; receipt download (GET /api/customer/transactions/{order_id}/receipt) remains dashboard-only and is not part of API key access.
curl -H "Authorization: Bearer zr_<public-id>_<secret>" \
"https://api.zrouter.dev/api/customer/transactions?limit=50"Usage analytics#
/api/customer/analyticsReturn aggregated usage analytics for a time range (default: the last 30 days), broken down by day, model, and API key, plus a comparison with the previous equal-length period.
Parameters
RFC3339 lower bound. Defaults to 30 days ago.
RFC3339 upper bound. Defaults to now. Must be after from.
Filter by model public name.
Filter by API key id (UUID). You can only see your own keys.
Responses
{
"summary": {
"requests": 210,
"tokens": 348000,
"cost_usd_micro": 25400,
"input_tokens": 300000,
"output_tokens": 48000,
"cache_read_tokens": 12000,
"cached_input_share": 0.04,
"blended_cost_usd_micro_per_million": 73000
},
"previous_summary": { ... },
"daily": [
{ "date": "2026-09-16", "requests": 18, "tokens": 26000, "cost_usd_micro": 1900,
"input_tokens": 22000, "output_tokens": 4000, "cache_read_tokens": 900 }
],
"breakdown_by_model": [
{ "model": "<public-name>", "requests": 180, "input_tokens": 250000,
"output_tokens": 45000, "cache_read_tokens": 11000, "cost_usd_micro": 22000 }
],
"breakdown_by_key": [
{ "api_key_id": "d1e2...", "name": "prod-key", "requests": 200,
"tokens": 340000, "cost_usd_micro": 24000 }
],
"token_breakdown": { "input_tokens": 300000, "output_tokens": 48000, "cache_read_tokens": 12000 }
}{ "error": { "code": "invalid_range", "message": "from must be before to" } }{ "error": { "code": "invalid_api_key", "message": "Invalid API key." } }curl -H "X-Api-Key: zr_<public-id>_<secret>" \
"https://api.zrouter.dev/api/customer/analytics?from=2026-09-01T00:00:00Z&to=2026-09-28T00:00:00Z"Export data#
/api/customer/exports/{kind}Download your data as a file attachment. The response carries Content-Disposition: attachment.
Parameters
One of usage, transactions, balances.
One of csv, json.
RFC3339 lower bound (usage and transactions only).
RFC3339 upper bound (usage and transactions only).
Responses
Content-Disposition: attachment; filename="usage-2026-09-28.csv"{ "error": { "code": "invalid_request", "message": "unsupported export kind" } }{ "error": { "code": "invalid_api_key", "message": "Invalid API key." } }curl -o usage.csv -H "Authorization: Bearer zr_<public-id>_<secret>" \
"https://api.zrouter.dev/api/customer/exports/usage?format=csv"