Usage & stats

How much each Application, API Key, and operation is being called, and the live counts a console’s home screen opens with.

  1. Endpoints
  2. GET /usage
  3. GET /stats

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 0 when nothing has it, so a client never has to check for a missing key.
  • subscriptions counts the subscriptions that cover their users (active, trialing, or past_due), by subscriber type. tier_change_requests.open counts requests that are pending, scheduled, or in_progress.
  • Fresh to the minute. The counts are computed at most once a minute per server instance; generated_at says when. Two calls a few seconds apart can return the same generated_at.
  • Platform-wide needs operations.view or users.list. An app-confined key never qualifies.
  • tenant_id confines 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, and organizations are then null, since they don’t belong to a Tenant. A Tenant the caller can’t see is 403 forbidden, or 404 tenant_not_found for a platform caller.