Usage & stats
How much each Application, API Key, and operation is being called, and the live counts a console’s home screen opens with.
Endpoints
| Method | Path | Requires | Purpose |
|---|---|---|---|
GET |
/usage |
operations.view; or an app-confined API Key (its own Application); or an Application’s owner via application_id |
Requests, errors, and latency per day, week, or month. |
GET |
/stats |
operations.view or users.list; or a Tenant’s owner via tenant_id |
Current counts by status, in one call. |
Both are reads, and both answer in the caller’s mode: a test-mode credential sees test-mode usage and counts only.
GET /usage
GET /usage?application_id=app_invoicer&group_by=operation&since=2026-10-01
// Response — 200
{
"since": "2026-10-01",
"until": "2026-10-28",
"granularity": "day",
"group_by": ["operation"],
"data": [
{ "period": "2026-10-01", "operation": "entitlements.check", "requests": 1840, "errors_4xx": 12, "errors_5xx": 0,
"latency_ms_avg": 38, "latency_ms_max": 412 },
{ "period": "2026-10-01", "operation": "users.list", "requests": 96, "errors_4xx": 0, "errors_5xx": 1,
"latency_ms_avg": 51, "latency_ms_max": 230 }
]
}
| Parameter | Notes |
|---|---|
since, until |
Dates (2026-10-01), UTC. since is included and until isn’t. until defaults to tomorrow, so today is included; since defaults to 30 days before until. until must be after since, and the range can be at most 13 months: otherwise 422 validation_failed (out_of_range). |
granularity |
day (default), week, or month. Weeks are ISO weeks, starting Monday. period is the bucket’s first day, so the buckets at either end of the range may be partial. |
group_by |
application, api_key, or operation. Repeat it to split by several (?group_by=application&group_by=operation). Each one adds its field to every row: application_id, api_key_id, or operation. Without it, there’s one row per period. |
application_id, api_key_id, operation |
Filters, exact match. operation is an operationId, such as users.list. |
Rows are oldest period first, then by the grouped fields. Periods and groups with no requests are left out. application_id is null for requests that didn’t resolve to exactly one Application (signing in, listing Users across Applications), and api_key_id is null for requests made with a User token or no credential.
Who sees what:
| Caller | Sees |
|---|---|
Platform operations.view |
Everything. |
| An app-confined API Key | Only its own Application’s rows, whatever it filters by. It needs no particular permission, since operations.view can’t go on an app-scoped key. |
An Application’s owner, with application_id=<their app> |
That Application’s rows. The owner is its owner_user_id, or an active org_admin of its owner_organization_id. |
| Anyone else | 403 forbidden. |
Where the numbers come from. Every API request writes one line to the request log (NFR → Observability). An hourly job rolls those lines up by UTC day, so today’s numbers run up to the last complete hour, and a day is final early the next morning. Requests rejected before they reach an operation (a missing Substratal-Version, an unknown path) aren’t counted. Usage is kept 13 months, and test-mode usage 30 days.
GET /stats
// GET /stats — Response 200
{
"tenant_id": null,
"generated_at": "2026-10-27T15:04:05Z",
"users": { "active": 1204, "invited": 31, "suspended": 4, "deleted": 17 },
"applications": {
"by_review_status": { "approved": 12, "pending_review": 2, "rejected": 1, "suspended": 0, "retired": 1 },
"by_access_tier": { "free": 5, "standard": 11 }
},
"entitlements": { "active": 3310, "disabled": 12, "expired": 40, "revoked": 95 },
"subscriptions": { "user": 210, "organization": 14 },
"organizations": { "active": 38, "suspended": 1 },
"webhook_subscriptions": { "healthy": 9, "unhealthy": 1, "disabled": 0 },
"push_credentials": { "active": 6, "failed": 0, "disabled": 1 },
"tier_change_requests": { "open": 1 }
}
- Every known status is present, with
0when nothing has it, so a client never has to check for a missing key. subscriptionscounts the subscriptions that cover their users (active,trialing, orpast_due), by subscriber type.tier_change_requests.opencounts requests that arepending,scheduled, orin_progress.- Fresh to the minute. The counts are computed at most once a minute per server instance;
generated_atsays when. Two calls a few seconds apart can return the samegenerated_at. - Platform-wide needs
operations.vieworusers.list. An app-confined key never qualifies. tenant_idconfines the counts to one Tenant: its Applications, and their Entitlements, webhook subscriptions, push credentials, and tier-change requests. The Tenant’s owner may call it, as well as the platform callers above.users,subscriptions, andorganizationsare thennull, since they don’t belong to a Tenant. A Tenant the caller can’t see is403 forbidden, or404 tenant_not_foundfor a platform caller.