Skip to main content

Usage Reporting

Read-only endpoints that surface credit consumption for the admin Usage dashboard and the in-app usage banners. 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 answer (an empty credit pool, or no alert) 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 day as a bare YYYY-MM-DD string.
  • 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-15",
"resourceType": "agent_run",
"totalCredits": "20.0000"
},
{
"date": "2026-01-15",
"resourceType": "app_compute",
"totalCredits": "50.0000"
},
{
"date": "2026-01-15",
"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-09",
"resourceType": "agent_run",
"totalCredits": "20.0000",
"totalEvents": 1
},
{
"month": "2026-09",
"resourceType": "ai",
"totalCredits": "130.0000",
"totalEvents": 2
},
{
"month": "2026-09",
"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.
  • Rows in both lists carry byCategory, the row's totalCredits split into ai, compute, and storage buckets by resource-type category. Usage whose resource type is no longer in the registry counts in totalCredits but in no bucket, so the segments can sum below the total.
  • 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,
"byCategory": {
"ai": 80,
"compute": 50,
"storage": 0
},
"totalEvents": 2
},
{
"entityId": "00000000-0000-4000-8000-000000000002",
"entityType": "app",
"name": "Sales Dashboard",
"slug": "sales-dashboard",
"totalCredits": 0.25,
"byCategory": {
"ai": 70,
"compute": 0,
"storage": 0
},
"totalEvents": 2
}
],
"topUsers": [
{
"actor": "marcus.webb",
"actorType": "user",
"displayName": "Marcus Webb",
"totalCredits": 0.25,
"byCategory": {
"ai": 80,
"compute": 50,
"storage": 0
},
"totalEvents": 2
},
{
"actor": "annie.larson",
"actorType": "user",
"displayName": "Annie Larson",
"totalCredits": 0.25,
"byCategory": {
"ai": 70,
"compute": 0,
"storage": 0
},
"totalEvents": 2
}
]
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/usage/alert-status​

The credit pool alert the in-app banners show billing admins, in the classic interface and in Informer GO. It applies the Usage Alert "Notify when" switches (billingConfig.alerts.thresholds) to the cached pool balance, so it answers with no License Manager call and is cheap to poll.

Authentication: Required

Pre-blocks: permission.billing.manage

Response:

A JSON object { state, percentUsed } (with a HAL _links.self).

FieldDescription
statenear-limit, exhausted, safety-buffer, or null when there is nothing to show
percentUsedShare of the pool consumed, 0 to 100 to one decimal place; null when no pool has been reported or the pool's grant is zero (state can still be exhausted)
  • near-limit starts at the lowest of the 80, 90, and 95 thresholds that is switched on (a switch that was never saved counts as on). With all three off there is no near-limit state.
  • exhausted (no credits remaining) and safety-buffer (no paid credits remaining, safety buffer still covering usage) are reported whatever the switches say, because features stop or are about to. The buffer amount itself is never exposed.
  • state is always null when the installation never draws from the credit pool.

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