Usage Reports Management API
Overview
The AI Budgets API answers what the ceilings are. This page answers the other half of the same question: how much of them is gone, and what a subject actually consumed in a period. It is the surface a FinOps owner automates against — chargeback exports, month-end reconciliation, "who is burning the budget" alerts — and it writes nothing.
Five endpoints, one response shape, five subjects: a consumer organization, a single consumer, a portal application, an API client, and an API proxy. Every address is under /apiops/projects/{projectName} and follows the shared Common Contract: the stable error envelope, correlation IDs, and the rule that a project outside the caller's scope is always 404 resourceNotFound, never 403. All five subjects are project-owned, so a call against the admin (platform) project is refused with 400 malformedRequest (params.reason = platformScopeNotSupported).
References are portable: a subject is addressed by its organization code, consumerKey, applicationKey, apiClientKey or API proxy name. No Mongo identifier, no traffic-log username and no quota identity value ever crosses the boundary.
Endpoints
| Method | Path | Subject | Breakdown |
|---|---|---|---|
GET | /credential-organizations/{organizationCode}/usage | One consumer organization | Per consumer |
GET | /consumers/{consumerKey}/usage | One consumer | — |
GET | /applications/{applicationKey}/usage | One portal application | Per API client |
GET | /api-clients/{apiClientKey}/usage | One API client | — |
GET | /api-proxies/{apiProxyName}/usage | One API proxy | — |
All five accept the same query parameters:
| Parameter | Default | Meaning |
|---|---|---|
from | 30 days before to | Start of the period. ISO-8601 UTC: 2026-08-01, 2026-08-01T00:00:00Z or an offset date-time such as 2026-08-01T03:00:00+03:00 |
to | now | End of the period, same spellings |
model | every model | Exact LLM model id, e.g. gpt-5. Narrows the whole report to that model's requests, and keeps the scope-total budget rows so a per-model figure still has something to be measured against |
environmentName | every environment of the project | Restricts the traffic-log blocks to one environment |
An inverted period (from after to), a period wider than 366 days, or an unparsable bound is refused with 400 malformedRequest rather than quietly corrected.
Response
{
"scope": { "type": "ORGANIZATION", "key": "acme-partners", "name": "acme-partners" },
"period": { "from": "2026-08-12T00:00:00Z", "to": "2026-09-11T00:00:00Z",
"effectiveFrom": "2026-08-12T00:00:00Z", "effectiveTo": "2026-09-11T23:59:59.999Z",
"timeZone": "UTC" },
"filters": { "model": null, "environmentName": null, "environmentApplied": false },
"ai": {
"dataSource": "ROLLUP_DAILY",
"granularity": "DAY",
"freshness": "hourly",
"totals": { "tokenInput": 4820113, "tokenOutput": 918204, "tokenCached": 120400,
"tokenTotal": 5858717, "costUsd": 214.382100, "requests": 20418, "failedRequests": 37 },
"byModel": [
{ "provider": "openai", "model": "gpt-5", "tokenInput": 4120113, "tokenOutput": 818204,
"tokenCached": 120400, "tokenTotal": 5058717, "costUsd": 198.210400, "requests": 17820 }
]
},
"aiLicensed": true,
"traffic": { "dataSource": "TRAFFIC_LOG", "totalRequests": 20418, "successfulRequests": 20381,
"blockedRequests": 0, "erroredRequests": 37,
"successRatePercent": 99.8, "avgTimeMs": 812.4 },
"children": {
"type": "CONSUMER",
"items": [ { "key": "con_8f3a1c", "name": "Ada Lovelace", "tokenTotal": 3120400,
"costUsd": 118.204000, "requests": 9820, "usagePercent": 53.3 } ],
"total": 14,
"truncated": false
},
"budgets": [
{ "family": "AI_COST", "unit": "USD", "window": "ONE_MONTH", "dimension": null,
"limit": 500.00, "consumed": 214.38, "remaining": 285.62, "usagePercent": 42.9,
"windowStartsAt": "2026-09-01T00:00:00Z", "resetsAt": "2026-10-01T00:00:00Z",
"assignmentKey": "lim_3d91", "planKey": "partner-monthly", "source": "TYPED" }
],
"budgetsAsOf": "2026-09-11T08:31:02Z",
"partial": false,
"unavailableEnvironments": [],
"notes": []
}
The three blocks, and where each comes from
| Block | Source | What it covers |
|---|---|---|
ai | Declared in ai.dataSource — see below | LLM token counts and cost in US dollars |
traffic | Always the Elasticsearch traffic log | Gateway request counts, success rate, average total time |
budgets | The quota usage snapshot | Live quota windows: ceiling, consumed, remaining, percentage, reset time |
ai.dataSource is ROLLUP_DAILY for a consumer, an organization and an AI proxy. Those subjects are read from the hourly token-usage rollup — the same table the AI Reports screens and the quota percentages use. It is independent of the traffic log's index-lifecycle retention, so a question about last quarter answers correctly instead of quietly short. Its grain is a UTC calendar day, so period.effectiveFrom/effectiveTo widen from/to to whole days and tell you which days were actually measured.
ai.dataSource is TRAFFIC_LOG for an application and an API client. The rollup carries no application or API-client dimension, so those two are read from the traffic log at exact timestamps (granularity: EXACT). Two consequences worth planning around: their AI figures are bounded by however long the traffic log is retained, and traffic written before 2026.09.2 carries no client attribution fields, so it can be matched only through the application's own credential usernames — an API client that existed before that release shows no usage for the period before it.
budgets ignores from/to entirely. A quota window is a live thing; the block describes the windows that are open right now, with budgetsAsOf naming how fresh the snapshot is (it is refreshed about once a minute). remaining is max(limit - consumed, 0), so a counter that overshot its ceiling reports 0 rather than a negative number.
Reading the other fields
filters.environmentAppliedisfalsewhenenvironmentNamewas sent but could not be applied to theaiblock, because the daily rollup has no environment dimension. The traffic block is still narrowed. The flag exists so a narrowed request never looks like it narrowed something it did not.childrencarries at most 200 rows, ordered by token total, and setstruncated: truewhenever rows were dropped — by that 200-row cap, or by the traffic log's own bucket window before the report ever saw them.totalis then a lower bound rather than an exact count, and the report is markedpartialwith thechild-list-truncatednote; whentruncatedisfalse,totalis exact.usagePercentis each row's share of the whole scope — including rows that were dropped and traffic that carried no client key — so the kept rows keep their true weight and the answered rows add up to less than 100% rather than being renormalised. A child is keyed by its own public key (consumerKey,apiClientKey) — never by a traffic-log username.partialistruewhen some source could not be read;unavailableEnvironmentsnames the environments whose traffic log failed. The call still answers200with what was readable: a FinOps report that returns a clean0for a source it never reached is worse than one that says it could not look.- Attribution prerequisites. The typed client attribution fields (
ack,cot,coi,coo) are stamped on a traffic-log document only when the request was authenticated by an OAuth2 or JWT policy that resolved a consumer or an API client (credential/ACL-managed mode). In managed from this policy mode with theclient_credentialsgrant the caller is the proxy's own application key, so no consumer attribution exists; Basic, API-key and third-party-IdP JWT traffic is attributed by username (uok) only. The Manager API Traffic list projection does not return these fields — open the document detail (or read Elasticsearch directly) to verify attribution. A source that rejects the query (for example an index mapping that cannot parse the range) is reported aspartial: truewith the environment inunavailableEnvironments, never as silent zeros. notesexplains a zero or a gap in machine-readable form:no-members(the organization currently has no consumer),not-ai-proxy(the proxy is not an AI proxy, soaiisnull),rollup-unavailable,member-list-truncated,child-list-truncated(the breakdown carries fewer rows than the scope has),model-list-truncated(more models were used thanai.byModelcan carry —ai.totalsstill covers all of them),ai-module-not-licensed,client-attribution-since-2026.09.2.aiLicensedreports whether the AI Gateway module is licensed on the installation. It is informational only — no block is withheld because of it; it simply explains why the AI figures are zero where the module is absent.
Budget rows
| Field | Meaning |
|---|---|
family | AI_TOKEN, AI_COST or REQUEST_QUOTA |
unit | TOKENS, USD or REQUESTS — matching the family |
window | ONE_HOUR, ONE_DAY or ONE_MONTH |
dimension | { "type": "MODEL" | "PROVIDER", "ref": "..." } for a per-model / per-provider sub-limit, null for the scope total |
assignmentKey | Public handle of the limit assignment the counter accrues under, when there is one |
planKey | Key of the limit plan that assignment references, when there is one |
source | TYPED (a limit assignment), LEGACY_BUDGET (an AI budget owner definition), POLICY (a summary of per-identity policy/rate-limit counters) |
A POLICY row is a summary, one per policy window, with limit and consumed left null and usagePercent carrying the worst percentage observed. Such a counter is keyed by a request identity — an IP address, a header value, a full Authorization value — which the platform stores encrypted precisely because it may embed a secret. Listing those identities on an automation surface would publish that set, and summing per-identity ceilings would be a number that means nothing.
Errors
Every error follows the Common Contract's error envelope — branch on errorKey, never on message.
| Error key | When |
|---|---|
malformedRequest | from is after to, the period is wider than 366 days, a date could not be parsed, or the admin (platform) project was addressed (params.reason = platformScopeNotSupported) |
resourceNotFound | An unknown organizationCode / consumerKey / applicationKey / apiClientKey / API proxy name, an environmentName that does not belong to the project, or a project outside the caller's scope |
permissionDenied | Authenticated, but the caller holds none of the categories this endpoint accepts |
An unreachable Elasticsearch cluster or a failed rollup read is not an error — it answers 200 with partial: true. See Common Contract → Error keys for the complete closed set.
Permissions & Licensing
- The four identity-shaped endpoints (organization, consumer, application, API client) accept the Identity or the AI Budgets permission at View. Either is enough: a budget owner and an identity owner both legitimately need this number, and neither should have to hold the other's category.
- The API proxy endpoint accepts API Management or Analytics at View — a proxy's traffic is API-management data, and the analytics role already reads exactly these figures on the reports screens.
- Every denial is the same
404 resourceNotFound, so a caller cannot learn which category it is missing. - No license module is required. The
aiLicensedflag is reported for context only; no part of the response is withheld on an installation without the AI Gateway module.
Example
# 1. Last 30 days of an organization, with its per-consumer breakdown
curl "https://apinizer.example.com/apiops/projects/finance/credential-organizations/acme-partners/usage" \
-H "Authorization: Bearer $TOKEN"
# 2. One consumer, one calendar month, one model
curl "https://apinizer.example.com/apiops/projects/finance/consumers/con_8f3a1c/usage?from=2026-08-01&to=2026-08-31&model=gpt-5" \
-H "Authorization: Bearer $TOKEN"
# 3. An application and the API clients under it, PROD only
curl "https://apinizer.example.com/apiops/projects/finance/applications/app_2d7e9b/usage?environmentName=PROD" \
-H "Authorization: Bearer $TOKEN"
# 4. One API client
curl "https://apinizer.example.com/apiops/projects/finance/api-clients/cli_74b2e0/usage?from=2026-09-01" \
-H "Authorization: Bearer $TOKEN"
# 5. One AI proxy — tokens, cost and traffic together
curl "https://apinizer.example.com/apiops/projects/finance/api-proxies/chat-gateway/usage" \
-H "Authorization: Bearer $TOKEN"
A month-end chargeback export is the organization call per organization, reading ai.totals.costUsd and the children rows; a budget-alarm job is any of the five, reading the budgets rows and branching on usagePercent and resetsAt.
Rankings and per-API breakdowns
The five endpoints above describe one subject at a time. A portal page or a FinOps board usually needs the other two shapes as well: who spent the most this month, and which APIs did this consumer actually use. Three more read-only endpoints answer exactly those, under the same /apiops/projects/{projectName} address and with the same period rules, filters and dataSource declaration.
| Method | Path | Answers |
|---|---|---|
GET | /usage/top | The period's ranking of one dimension by one metric, with each row's share of the total |
GET | /usage/consumers/{consumerKey}/api-proxies | Which API proxies one consumer called, and how much |
GET | /usage/applications/{applicationKey}/api-proxies | Which API proxies one application called, and how much |
Ranking — GET /usage/top
| Parameter | Default | Meaning |
|---|---|---|
dimension | required | consumers, applications, api-clients, models or api-proxies. Any other value is 400 malformedRequest (params.parameter = dimension) |
metric | tokens | tokens, cost or requests. Decides both the ordering and what value / sharePercent mean |
organization | every organization | A consumer organization code; narrows the ranking to that organization's traffic |
limit | 50 | Rows the answer may carry, at most 500. A value above the ceiling is clamped, not refused |
from, to, model, environmentName | as above | The same period and filter rules as the five subject endpoints |
{
"dimension": "consumers",
"metric": "cost",
"unit": "USD",
"period": { "from": "2026-08-12T00:00:00Z", "to": "2026-09-11T00:00:00Z",
"effectiveFrom": "2026-08-12T00:00:00Z", "effectiveTo": "2026-09-11T23:59:59.999Z",
"timeZone": "UTC" },
"filters": { "organizationCode": "acme-partners", "environmentApplied": false },
"dataSource": "ROLLUP_DAILY",
"freshness": "hourly",
"total": 1284.502310,
"limit": 50,
"truncated": false,
"items": [
{ "rank": 1, "key": "con_8f3a1c", "name": "Ayse Yilmaz", "value": 612.400000,
"sharePercent": 47.6, "tokenTotal": 18450233, "costUsd": 612.400000, "requests": 91204 },
{ "rank": 2, "key": "con_2b71de", "name": "billing-batch", "value": 388.102310,
"sharePercent": 30.2, "tokenTotal": 11002914, "costUsd": 388.102310, "requests": 40551 }
],
"partial": false,
"unavailableEnvironments": [],
"notes": []
}
Three properties of this answer are worth reading carefully, because a chart drawn from it depends on them:
totalis the whole dimension, not the rows. It is summed over every matching row beforelimitis applied, and it also contains consumption that could not be attributed to a nameable row. The shares of a truncated answer therefore add up to less than 100%. They are floored to one decimal, never rounded, so the sum can never exceed 100 either.truncatedis the only honest signal that rows were left out. A full-looking list oflimitrows is not evidence that nothing was dropped.dataSourcesays which source was read.consumers,modelsandapi-proxiescome from the daily AI usage rollup — written hourly, kept independently of the traffic log's retention, and the same numbers the AI Reports screen shows.applicationsandapi-clientshave no rollup scope at all and are read from the traffic log, so they are bounded by its retention and, for traffic written before 2026.09.2, attributable only through the owner's own credentials (notescarriesclient-attribution-since-2026.09.2). On a rollup dimensionmetric=requestscounts AI-attributed requests only, which the noterequests-are-ai-attributedstates.
A row whose source reference cannot be translated into a public key — an unresolved rollup reference, an application that has since been deleted — is not itemised; it stays inside total and notes carries unresolved-rows-dropped. This is deliberate: the reference may be a raw traffic-log username, which never crosses the API boundary.
Per-API breakdown — GET /usage/{consumers|applications}/{key}/api-proxies
Accepts from, to, model, environmentName and limit (default 50, at most 500).
{
"scope": { "type": "CONSUMER", "key": "con_8f3a1c", "name": "Ayse Yilmaz" },
"period": { "from": "2026-08-12T00:00:00Z", "to": "2026-09-11T00:00:00Z", "timeZone": "UTC" },
"trafficDataSource": "TRAFFIC_LOG",
"aiDataSource": "ROLLUP_DAILY",
"limit": 50,
"truncated": false,
"items": [
{ "proxyName": "chat-gateway", "requests": 84120, "failedRequests": 412,
"successRatePercent": 99.5, "avgTimeMs": 812.4,
"ai": { "tokenTotal": 18450233, "costUsd": 612.400000 } },
{ "proxyName": "petstore", "requests": 7084, "failedRequests": 0,
"successRatePercent": 100.0, "avgTimeMs": 41.2 }
],
"partial": false,
"notes": []
}
ai is absent, not a row of zeros, on a proxy that carried no AI traffic for this subject — a block of zeros would read as "an AI proxy nobody used". For a consumer the AI half comes from the rollup, so a proxy whose traffic has already aged out of the log still appears with its token and cost picture and requests: 0. For an application both halves come from the traffic log, which is why that answer carries the attribution note.
Errors and permissions
The error envelope, the 404 for a project outside the caller's scope and the 400 malformedRequest for the admin project are exactly those of the five endpoints above. dimension and metric add one case each: an unknown value is 400 malformedRequest with params.parameter naming which one. Authorization is decided before the dimension is validated, so a caller outside the project always receives the same 404 and learns nothing from the difference.
Permissions follow the same pairing: dimension=api-proxies accepts API Management or Analytics at View; every other dimension and both breakdown endpoints accept Identity or AI Budgets at View.
Examples
# Top 10 consumers by cost this month, inside one organization
curl "https://apinizer.example.com/apiops/projects/finance/usage/top?dimension=consumers&metric=cost&organization=acme-partners&from=2026-09-01&limit=10" \
-H "Authorization: Bearer $TOKEN"
# Which models the project spent its tokens on
curl "https://apinizer.example.com/apiops/projects/finance/usage/top?dimension=models&metric=tokens" \
-H "Authorization: Bearer $TOKEN"
# Busiest API clients, PROD only
curl "https://apinizer.example.com/apiops/projects/finance/usage/top?dimension=api-clients&metric=requests&environmentName=PROD" \
-H "Authorization: Bearer $TOKEN"
# Which APIs one consumer used
curl "https://apinizer.example.com/apiops/projects/finance/usage/consumers/con_8f3a1c/api-proxies?from=2026-09-01" \
-H "Authorization: Bearer $TOKEN"
# Which APIs one application used
curl "https://apinizer.example.com/apiops/projects/finance/usage/applications/app_2d7e9b/api-proxies" \
-H "Authorization: Bearer $TOKEN"
Trends, hierarchy and budget boards
The endpoints above answer one figure at a time. A page embedded in a customer portal or a FinOps board usually needs three more shapes: how did this move over the period, how does a group break down, and who is about to run out. Three read-only views answer exactly those, under the same address as the subject they describe — the summary card is .../usage, and each view appends its own segment.
| Method | Path | Answers |
|---|---|---|
GET | /{collection}/{key}/usage/timeseries | The period trend of one subject, bucketed by day, week or month |
GET | /credential-organizations/{organizationCode}/usage/hierarchy | The sub-organization tree with rolled-up totals, or one page of the organization's consumers |
GET | /{organizations|applications}/{key}/usage/budget-board | One row per direct child, carrying its live quota window closest to the limit |
{collection} is the same set as above: credential-organizations, consumers, applications, api-clients, api-proxies.
Trend — GET .../usage/timeseries
| Parameter | Default | Meaning |
|---|---|---|
from, to | last 30 days | As above. In addition, the period may not span more than 366 UTC calendar days — every source here is grained by whole days |
granularity | day | day, week or month |
include | ai,traffic | Which blocks to fill. A block that was not requested is answered as null, never as 0 |
model, environmentName | — | As above |
{
"scope": { "type": "CONSUMER", "key": "con_8f3a1c", "name": "Ada Lovelace" },
"period": { "from": "2026-09-01T00:00:00Z", "to": "2026-09-04T00:00:00Z",
"effectiveFrom": "2026-09-01T00:00:00Z", "effectiveTo": "2026-09-04T23:59:59.999Z",
"timeZone": "UTC" },
"filters": { "model": null, "environmentName": null, "environmentApplied": false },
"granularity": "DAY",
"aiDataSource": "ROLLUP_DAILY",
"aiFreshness": "hourly",
"trafficDataSource": "TRAFFIC_LOG",
"series": [
{ "bucketStart": "2026-09-01T00:00:00Z", "tokenInput": 120400, "tokenOutput": 24100,
"tokenCached": 0, "tokenTotal": 144500, "costUsd": 5.412000,
"aiRequests": 812, "aiFailedRequests": 3,
"requests": 840, "failedRequests": 5, "avgTimeMs": 742.1 },
{ "bucketStart": "2026-09-02T00:00:00Z", "tokenInput": 0, "tokenOutput": 0,
"tokenCached": 0, "tokenTotal": 0, "costUsd": 0.000000,
"aiRequests": 0, "aiFailedRequests": 0,
"requests": 0, "failedRequests": 0, "avgTimeMs": null }
],
"total": 4,
"partial": false,
"unavailableEnvironments": [],
"notes": []
}
Every bucket of the period is present, including the quiet ones — the example above lists the first two of the period's four daily buckets, which is why total reads 4. A source can only report the days it actually holds documents for; the series is therefore generated from the requested window and filled with whatever came back. That is what makes 0 and unread two different answers: a zero bucket means the day existed and nothing happened, while a source that could not be read sets partial: true and names the environment in unavailableEnvironments. avgTimeMs is null rather than 0 for a bucket with no request — an average that was never measured is not zero.
A week starts on Monday and a month on the 1st, in UTC (ISO-8601, the same convention Elasticsearch uses). A period that begins mid-week therefore opens with a bucket whose bucketStart is the preceding Monday; that bucket carries only the days inside the period, so a chart must label it as the calendar week it belongs to rather than as a whole week of traffic.
The AI block and the traffic block count different things. tokenTotal / costUsd / aiRequests are LLM consumption; requests, failedRequests and avgTimeMs are gateway traffic. An AI proxy that also fronts non-AI endpoints has traffic without tokens, which is why the two are never merged into a single requests field. aiDataSource declares where the AI half came from, exactly as ai.dataSource does on the summary card — ROLLUP_DAILY for a consumer, an organization and an AI proxy; TRAFFIC_LOG for an application and an API client.
Hierarchy — GET /credential-organizations/{code}/usage/hierarchy
| Parameter | Default | Meaning |
|---|---|---|
from, to | last 30 days | As above, capped at 366 calendar days |
level | organization | organization for the subtree, consumer for the paged consumer list |
includeDescendants | false | With level=consumer, also list the consumers of descendant organizations |
page, size | 0, 20 (max 200) | Paging of the consumer list |
{
"scope": { "type": "ORGANIZATION", "key": "acme-partners", "name": "acme-partners" },
"level": "ORGANIZATION",
"dataSource": "ROLLUP_DAILY",
"freshness": "hourly",
"nodes": [
{ "code": "acme-partners", "name": "Acme Partners", "classification": "ORGANIZATION",
"depth": 0, "shared": false, "memberCount": 3, "descendantMemberCount": 11,
"direct": { "tokenTotal": 144500, "requests": 812, "failedRequests": 3, "costUsd": 5.412000 },
"rollup": { "tokenTotal": 982400, "requests": 5140, "failedRequests": 21, "costUsd": 38.104000 },
"budgetPercent": 49.1,
"children": [ { "code": "acme-emea", "classification": "ORGANIZATION", "depth": 1, "…": "…" } ] }
],
"partial": false,
"notes": []
}
At every node rollup equals direct plus the sum of the children's rollup. That is the property the whole view exists for: an owner reads a parent row, expands it, and the children must add up. direct is what the organization's own consumers spent; rollup is the whole subtree.
Nodes are addressed by organization code, never by an internal identifier. classification says what a node is: a real ORGANIZATION, or one of the two synthetic buckets the report produces for consumption that belongs to no organization (UNASSIGNED) and for traffic that carried no consumer identity at all (ANONYMOUS). Those buckets exist so that no spend is silently dropped from the totals.
With level=consumer the answer carries members instead of nodes:
{
"level": "CONSUMER",
"members": {
"content": [
{ "consumerKey": "con_8f3a1c", "name": "Ada Lovelace", "organizationCode": "acme-partners",
"organizationPath": "Acme / Partners",
"usage": { "tokenTotal": 144500, "requests": 812, "failedRequests": 3, "costUsd": 5.412000 },
"budgetPercent": 28.9 }
],
"totalElements": 11, "page": 0, "size": 20
}
}
A consumer that no longer resolves — a deleted account, or a usage row the rollup could only file under a username — keeps its figures and answers consumerKey: null with the note unresolved-members. The figures stay so the members still add up to the organization; the key is withheld because the only identifier left is a traffic-log username, which never crosses this boundary.
The figures come from the hourly token rollup, which has no environment dimension, so this endpoint accepts no environmentName and filters.environmentApplied is always false. budgetPercent is filled only when the requested period sits inside a single calendar month — a monthly ceiling cannot be compared against a multi-month window.
Budget board — GET .../usage/budget-board
Available for an organization (rows are its consumers) and for an application (rows are its API clients).
| Parameter | Default | Meaning |
|---|---|---|
page, size | 0, 20 (max 200) | Paging |
sort | percent,desc | percent[,asc|desc]; name is always accepted as the tie-breaker |
{
"scope": { "type": "ORGANIZATION", "key": "acme-partners", "name": "acme-partners" },
"childType": "CONSUMER",
"summary": { "total": 11, "ok": 7, "warning": 2, "exceeded": 1, "noBudget": 1 },
"items": [
{ "key": "con_8f3a1c", "name": "Ada Lovelace",
"family": "AI_COST", "unit": "USD", "window": "ONE_MONTH",
"limit": 500.00, "consumed": 512.40, "remaining": 0.00, "percent": 102.5,
"windowStartsAt": "2026-09-01T00:00:00Z", "resetsAt": "2026-10-01T00:00:00Z",
"assignmentKey": "lim_3d91", "planKey": "partner-monthly", "source": "TYPED",
"status": "EXCEEDED" }
],
"totalElements": 11, "page": 0, "size": 20,
"asOf": "2026-09-11T08:31:02Z",
"notes": []
}
The board is independent of from/to and accepts neither: these are the quota windows the gateway is counting into right now, and asOf says how far behind the snapshot is (about a minute).
One row per child, carrying its worst live window. A child may own several at once — a monthly token ceiling, a monthly cost ceiling, a daily request quota — and the row shows whichever is closest to its limit, with family and unit naming the number. Without them a bare 512.40 could be dollars, tokens or requests.
status is OK, WARNING at 80% or more, EXCEEDED at 100% or more, and NO_BUDGET when the child has no live ceiling at all. A child with no ceiling is still a row rather than a gap — a board that hid it would quietly suggest it was fine — and summary counts every child, not only the page that was answered, so an owner can reconcile the counters against his own expectation before paging through the rows.
Two kinds of counter are deliberately not eligible for a board row. A per-model sub-limit is a slice of the child's budget rather than the budget, and ranking children by a slice would put a child that is 90% through one model above a child that is 99% through everything. And a policy or rate-limit counter is keyed by a request identity — an IP address, a header value, a whole Authorization value — which the platform stores encrypted precisely because it may embed a secret; such a counter describes a caller, not the child, and never reaches this board.
Examples
# Weekly trend of one consumer, tokens only
curl "https://apinizer.example.com/apiops/projects/finance/consumers/con_8f3a1c/usage/timeseries?from=2026-07-01&to=2026-09-30&granularity=week&include=ai" \
-H "Authorization: Bearer $TOKEN"
# Daily traffic trend of an AI proxy in one environment
curl "https://apinizer.example.com/apiops/projects/finance/api-proxies/chat-gateway/usage/timeseries?granularity=day&include=traffic&environmentName=PROD" \
-H "Authorization: Bearer $TOKEN"
# The organization tree for last month
curl "https://apinizer.example.com/apiops/projects/finance/credential-organizations/acme-partners/usage/hierarchy?from=2026-08-01&to=2026-08-31" \
-H "Authorization: Bearer $TOKEN"
# Its consumers, including every sub-organization, second page
curl "https://apinizer.example.com/apiops/projects/finance/credential-organizations/acme-partners/usage/hierarchy?level=consumer&includeDescendants=true&page=1&size=50" \
-H "Authorization: Bearer $TOKEN"
# Who is about to run out, worst first
curl "https://apinizer.example.com/apiops/projects/finance/credential-organizations/acme-partners/usage/budget-board" \
-H "Authorization: Bearer $TOKEN"
# The API clients of one application, alphabetically
curl "https://apinizer.example.com/apiops/projects/finance/applications/app_2d7e9b/usage/budget-board?sort=name,asc" \
-H "Authorization: Bearer $TOKEN"
A portal chart is the trend call per subject; a FinOps drill-down is the hierarchy call, expanding a node into level=consumer; a "who is about to run out" alarm is the budget board, reading summary.exceeded and summary.warning before it ever pages through items.
Errors and permissions
These three views share the summary card's gate exactly — the same permission pairs, the same 404 for every denial, the same 400 malformedRequest for the admin project. They add three more malformedRequest cases: an unknown granularity, an unknown token in include, and an unknown level. A misspelled parameter is refused rather than silently ignored, because a caller who believes his filter worked reads the wrong number. An unknown sort column on the budget board is unsupportedSortField.
Related Documentation
- AI Budgets API - The ceilings themselves: scope rules, hierarchy, effective limit, and the budget update
- Limit Assignments Management API - The assignment whose public handle appears as
assignmentKeyon a budget row - Consumers & Applications Management API - The owners addressed by
consumerKeyandapplicationKeyon this page - API Clients API - The client addressed by
apiClientKey, and its migration from a legacy credential - Reports API - Inventory reports (what exists) as opposed to the consumption reports on this page
- Common Contract - The shared error envelope, correlation IDs, and status-code rules every endpoint on this page follows