Skip to main content

Usage Reporting

Read-only endpoints that surface credit consumption for the admin Usage dashboard. All consumption is recorded in the append-only usage_event ledger and aggregated on read.

Every endpoint on this page is a plain GET that returns a JSON object or array, so each one answers 200. None of them are AI-gated: AI does not need to be enabled on the tenant. They read the local entitlement cache (a findOne/create, with no License Manager round-trip), so a tenant that has never configured AI still gets a valid (empty) credit pool back rather than an error.

The usage ledger

Every metered activity appends one row to usage_event with the actor, resource type, credits charged, and resource-specific metadata. AI events are mirrored into the ledger from the AI audit log so all resource types share a single read path. The reporting endpoints below aggregate this ledger.


GET /api/usage

Aggregated credit usage for a date range, plus current budgets, credit pool, and License Manager sync health. Defaults to the current month when no range is supplied.

Authentication: Required

Pre-blocks: permission.billing.manage

Query Parameters:

ParameterTypeDefaultDescription
startDateISO datestart of current monthRange start (inclusive)
endDateISO datenowRange end (inclusive)

Response:

A JSON object { usageSummary, dailyTotals, budgets, creditPool, sync } (with a HAL _links.self).

  • usageSummary rows group by (entityId, entityType, resourceType) and carry totalCredits and totalEvents, ordered by entityId then resourceType. totalCredits is serialized as a fixed-precision string (for example "50.0000").
  • dailyTotals rows are { date, resourceType, totalCredits }, where date is the bucket timestamp.
  • Each budgets entry carries the stored budget columns (monthlyBudget, creditsUsed) plus the computed getters isOverBudget, budgetPercent, and budgetRemaining, and an entity sub-object ({ name, slug, ownerId }) enriched from the App row. The entity is null for non-app budgets or for a budget whose entityId no longer matches an App.
  • creditPool is the normalized pool with the internal slush field stripped.
  • sync surfaces License Manager connectivity.
Get aggregated usageGET /api/usage
GET /api/usage (permission.billing.manage) returns { usageSummary, dailyTotals, budgets, creditPool, sync }. Defaults to the current month; `startDate`/`endDate` are optional ISO-date query params. `usageSummary` rows group by (entityId, entityType, resourceType) with totalCredits/totalEvents; `dailyTotals` rows are { date, resourceType, totalCredits } (bare YYYY-MM-DD). Each `budgets` entry carries the budget columns plus computed isOverBudget/budgetPercent/budgetRemaining and an `entity` sub-object ({ name, slug, ownerId }) for app budgets (null otherwise). `creditPool` has internal `slush` stripped; `sync` surfaces LM connectivity (lastSyncFailedAt is null when the last sync succeeded; lastSyncFailureCode is one of lm_unreachable/lm_unavailable/lm_error/license_not_recognized).
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage"
}
},
"usageSummary": [
{
"entityId": "00000000-0000-4000-8000-000000000001",
"entityType": "app",
"resourceType": "agent_run",
"totalCredits": "20.0000",
"totalEvents": 1
},
{
"entityId": "00000000-0000-4000-8000-000000000001",
"entityType": "app",
"resourceType": "ai",
"totalCredits": "50.0000",
"totalEvents": 1
},
{
"entityId": "00000000-0000-4000-8000-000000000002",
"entityType": "app",
"resourceType": "ai",
"totalCredits": "80.0000",
"totalEvents": 1
},
"… 1 more items (4 total) — omitted from docs"
],
"dailyTotals": [
{
"date": "2026-01-15T16:20:00.000Z",
"resourceType": "agent_run",
"totalCredits": "20.0000"
},
{
"date": "2026-01-15T16:20:00.000Z",
"resourceType": "app_compute",
"totalCredits": "50.0000"
},
{
"date": "2026-01-15T16:20:00.000Z",
"resourceType": "ai",
"totalCredits": "130.0000"
}
],
"budgets": [
{
"entityId": "00000000-0000-4000-8000-000000000001",
"entityType": "app",
"monthlyBudget": 2000,
"creditsUsed": 1250.5,
"isOverBudget": false,
"budgetPercent": 62.525,
"budgetRemaining": 749.5,
"entity": {
"name": "Sales Dashboard",
"slug": "sales-dashboard",
"ownerId": "admin"
}
}
],
"creditPool": {
"included": 0,
"used": 0,
"remaining": 0
},
"sync": {
"syncedAt": null,
"lastSyncFailedAt": null,
"lastSyncFailureCode": null,
"stalenessLevel": "expired"
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

The sync block lets the UI render a "license server unreachable" banner. lastSyncFailedAt is null when the most recent sync succeeded; lastSyncFailureCode is one of lm_unreachable, lm_unavailable, lm_error, or license_not_recognized. stalenessLevel reflects how current the cached entitlement is (for example expired when the tenant has never completed a sync).


GET /api/usage/monthly

Credit usage grouped by month and resource type, for the historical trend chart.

Authentication: Required

Pre-blocks: permission.billing.manage

Query Parameters:

ParameterTypeDefaultDescription
monthsinteger (1-24)12Number of trailing months to include

Response:

A JSON object { history, creditPool } (with a HAL _links.self). Each history row is { month, resourceType, totalCredits, totalEvents }, where month is a YYYY-MM string and totalCredits is a fixed-precision string. Rows are ordered by month descending, then resourceType. creditPool is the same normalized pool returned by GET /api/usage.

Get monthly usage trendGET /api/usage/monthly?months=12
GET /api/usage/monthly (permission.billing.manage) returns { history, creditPool }. `months` is an integer query param (1..24, default 12). Each `history` row is { month: "YYYY-MM", resourceType, totalCredits, totalEvents }, ordered by month DESC then resourceType.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/monthly"
}
},
"history": [
{
"month": "2026-06",
"resourceType": "agent_run",
"totalCredits": "20.0000",
"totalEvents": 1
},
{
"month": "2026-06",
"resourceType": "ai",
"totalCredits": "130.0000",
"totalEvents": 2
},
{
"month": "2026-06",
"resourceType": "app_compute",
"totalCredits": "50.0000",
"totalEvents": 1
}
],
"creditPool": {
"included": 0,
"used": 0,
"remaining": 0
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/usage/top-consumers

Top apps and top users (or agents) by credit consumption over a rolling window.

Authentication: Required

Pre-blocks: permission.billing.manage

Query Parameters:

ParameterTypeDefaultDescription
daysinteger (1-365)30Rolling window size
limitinteger (1-50)10Max rows per list

Response:

A JSON object { windowDays, startDate, endDate, topApps, topUsers } (with a HAL _links.self). startDate and endDate are bare YYYY-MM-DD strings bounding the window.

  • topApps rows enrich name and slug from the App row; both are null when no App matches the entityId.
  • topUsers rows resolve displayName from the User row, falling back to the actor id when no User matches.
  • Both lists are ordered by totalCredits descending.
Get top consumersGET /api/usage/top-consumers?days=30&limit=10
GET /api/usage/top-consumers (permission.billing.manage) returns { windowDays, startDate, endDate, topApps, topUsers }. `days` (1..365, default 30) and `limit` (1..50, default 10) are query params. topApps rows enrich name/slug from the App row (null when no App matches the entityId); topUsers rows resolve displayName from the User row (falling back to the actor id). Both lists are ordered by totalCredits DESC.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/top-consumers"
}
},
"windowDays": 30,
"startDate": "2026-01-15",
"endDate": "2026-01-15",
"topApps": [
{
"entityId": "00000000-0000-4000-8000-000000000001",
"entityType": "app",
"name": "Ops Monitor",
"slug": "ops-monitor",
"totalCredits": 0.25,
"totalEvents": 2
},
{
"entityId": "00000000-0000-4000-8000-000000000002",
"entityType": "app",
"name": "Sales Dashboard",
"slug": "sales-dashboard",
"totalCredits": 0.25,
"totalEvents": 2
}
],
"topUsers": [
{
"actor": "marcus.webb",
"actorType": "user",
"displayName": "Marcus Webb",
"totalCredits": 0.25,
"totalEvents": 2
},
{
"actor": "annie.larson",
"actorType": "user",
"displayName": "Annie Larson",
"totalCredits": 0.25,
"totalEvents": 2
}
]
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/usage/resource-types

The canonical resource type registry. Read-only: types are defined in code and cannot be created or edited.

Authentication: Required

Pre-blocks: permission.tenant.superuser

Stricter than the other usage reads

This endpoint is gated by permission.tenant.superuser, not permission.billing.manage, because the registry is system metadata rather than billing data. A billing manager who is not a superuser cannot read it.

Response:

A plain JSON array of { slug, name, category, mode, unit } sourced from the in-code resource-type registry. There is no database table behind it, so there is nothing to create or edit.

List resource typesGET /api/usage/resource-types
GET /api/usage/resource-types is gated by permission.tenant.superuser (stricter than the other usage reads, which need only billing.manage) because the registry is system metadata, not billing data. It returns a plain array of { slug, name, category, mode, unit } sourced from the in-code RESOURCE_TYPES registry — read-only, with no create/edit. `category` is ai|compute|storage, `mode` is event (per-occurrence) or gauge (point-in-time).
Response · 200
[
{
"slug": "ai",
"name": "AI",
"category": "ai",
"mode": "event",
"unit": "dollar"
},
{
"slug": "agent_run",
"name": "Agent Run",
"category": "ai",
"mode": "event",
"unit": "run"
},
{
"slug": "app_compute",
"name": "App Compute",
"category": "compute",
"mode": "event",
"unit": "second"
},
"… 5 more items (8 total) — omitted from docs"
]
Captured from the API examples test suite; ids and timestamps are normalized.
FieldDescription
slugStable identifier used on usage events
nameDisplay name
categoryai, compute, or storage
modeevent (per-occurrence) or gauge (point-in-time)
unitUnit of the raw measurement before rate application