One row per key: each key's usage and cost over the period, for every key that matches your filters. Revoked and expired keys are included. For one key, use GET /v1/keys/{key_id}/usage. How to turn the rows into invoices is on Bill your customers per device.
https://api.decisionnode.com /v1/usagecurl -G https://api.decisionnode.com/v1/usage \
-H "Authorization: Bearer $DECISIONNODE_MANAGE_KEY" \
--data-urlencode "start=2026-10-01T00:00:00Z" \
--data-urlencode "end=2026-11-01T00:00:00Z" \
--data-urlencode "label=customer:acme"Headers#
AuthorizationheaderrequiredBearera Full access or Manage device keys key, on your server. A Decisions-only key gets403key_scope_denied, "This key may only make decisions.", and a Publishable key "A publishable key may only make decisions."
Query parameters#
startstringrequired- RFC 3339, with any offset, read in UTC and rounded down to the hour: the first moment counted.
endstringrequired- RFC 3339, rounded up to the hour: the first moment not counted, so a month is
2026-10-01T00:00:00Zto2026-11-01T00:00:00Z. A period is at most 400 days. There is no limit on how far back it starts: usage is kept indefinitely. granularitystringdefault"none"none,hour,dayormonth:noneanswers the totals only; the others addbucketsof an hour, a day or a calendar month, in UTC.hourtakes a period of at most 7 days.group_bystringmodelorchannel: addsby_modelorby_channel, the totals split by model or by channel.labelstringname:value, such ascustomer:acme. Repeat it: a key must match every one, as on the keys list.device_idstring- The key with this
device_id: 1 to 128 printable ASCII characters. key_idstring- One key,
key_and 32 hex digits; repeat it for up to 100. scopestring- One kind:
full,decideorkeys. statusstringdefault"all"active,expired,revokedorall: what the key is now, not what it was in the period.include_emptybooleandefaultfalsetruelists the keys with no usage in the period too, each with totals of 0.limitintegerdefault100- Keys a page, 1 to 1,000; with a
granularityother thannone, at most 100. A page can hold fewer: see Paging. cursorstring- The
next_cursorof the page before.
Response#
200 OK with the period read, how current the figures are, and a page of rows, newest key first: pass next_cursor as cursor for the next page, until it is null. A key with no usage in the period has no row unless include_empty=true.
{
"period": {
"start": "2026-10-01T00:00:00Z",
"end": "2026-11-01T00:00:00Z"
},
"as_of": "2026-11-01T00:00:00Z",
"items": [
{
"key": {
"id": "key_3b8f0d6e2a914c7b9e5f1a2c4d6e8f01",
"name": "Drone 0042",
"scope": "decide",
"device_id": "acme-drone-0042",
"labels": { "customer": "acme", "plan": "pro" },
"status": "active",
"created_at": "2026-09-02T08:14:00Z",
"revoked_at": null,
"expires_at": null
},
"totals": {
"requests": 12480,
"free_tier_requests": 0,
"input_tokens": 68640000,
"image_tokens": 49920000,
"video_tokens": 0,
"booster_calls": 0,
"cost": { "usd": 2.88288, "model_usd": 2.88288, "boosters_usd": 0 }
}
},
{
"key": {
"id": "key_9d1c5e7a3b2f4e6d8c0a1b3e5f7a9c20",
"name": "Drone 0017",
"scope": "decide",
"device_id": "acme-drone-0017",
"labels": { "customer": "acme", "plan": "pro" },
"status": "revoked",
"created_at": "2026-08-11T10:02:00Z",
"revoked_at": "2026-10-19T16:40:00Z",
"expires_at": null
},
"totals": {
"requests": 5210,
"free_tier_requests": 0,
"input_tokens": 28655000,
"image_tokens": 20840000,
"video_tokens": 0,
"booster_calls": 0,
"cost": { "usd": 1.20351, "model_usd": 1.20351, "boosters_usd": 0 }
}
}
],
"next_cursor": null
}periodobject{"start", "end"}as applied:startrounded down andendup to the hour, in UTC.as_ofstring- RFC 3339, never past
period.end: the figures are complete up to here, and a call after it may not be counted yet. Onceas_ofequalsperiod.end, the period's figures are final: see as_of. itemsarray- One row per key.
keyobject- The key, as it is now: revoked and expired keys included.
id, name, scopestring- As on the key object.
device_idstring or nullnullon a key without one.labelsobject{}on a key without any.statusstringactive,expiredorrevoked.created_at, revoked_at, expires_atstring or null- RFC 3339.
totalsobject- The key's totals for the period.
requestsinteger- Every call the key made, answered or not: decisions (refused and failed ones included) and session opens. A session's frames are not counted. Refused and failed calls cost nothing.
free_tier_requestsinteger- Of those, the requests whose model part the free tier covered, at no cost; their booster calls past the day's free booster calls are billed.
input_tokensinteger- Every input token, as each answer's
usage.input_tokenscounts it, image and video tokens included. A refused or failed decision's tokens count. There is no output count: output is free. image_tokens, video_tokensinteger- The images' and the videos' shares of
input_tokens. booster_callsinteger- The booster calls billed, collection enrollments included.
costobject{"usd", "model_usd", "boosters_usd"}, in USD, written as an answer'susage.costis but in whole micro-dollars, so at most 6 decimals, and without itsfree_tierflag (free_tier_requestscounts those calls): what your workspace is charged for these calls,usd=model_usd+boosters_usd. See the cost.
bucketsarray- With a
granularityother thannone:{"start", "end", "totals"}for each hour, day or month that has usage, oldest first, in UTC. The first and the last are cut to the period. by_modelarray- With
group_by=model:{"model", "totals"}for each model the key used, in name order. by_channelarray- With
group_by=channel:{"channel", "totals"}for all three channels, always, a quiet one at 0:livefor decisions and every other call (collection enrollments and the free tier's requests included),sessionfor sessions (an open counted as a request, its frames' tokens and cost included) andbatchat 0.
next_cursorstring or null- The next page's
cursor;nullon the last page.
Paging#
Keys come newest first. A page reads at most 150,000 hourly rows: its keys, times the period's hours, times the models each key has used (counted to 16). So a page can hold fewer keys than limit: over a month, about 201 keys that each used one model, or 67 that used three; over 400 days, 15. It always holds at least one key. Follow next_cursor until it is null, and never stop at a short page.
The cost#
totals.cost is what your workspace is charged for the key's calls in the period: refused and failed calls, and the free tier's model part, at 0, and a capped key's answers at most what was reserved for them. On a prepaid workspace it is the list price as metered: what your balance was drawn down by, and your usage page's spend. On a workspace invoiced at a negotiated rate, it is the invoice's own arithmetic at the month's rate: summed over your keys, it matches the usage on your invoice, each answer's usage.cost, and your usage page's spend, which on such a workspace runs a couple of minutes behind. An invoice's workspace-wide lines (reserved capacity, collection storage, and the credit for usage drawn before invoicing began) are in no key's figures. Totals are priced per key, month and model, and each bucket and split on its own sums: at list prices they add up exactly; at a negotiated rate a part can differ from its share by a micro-dollar.
as_of#
- Complete up to
as_of. Usage is folded in every minute, from calls at least two minutes old, andas_ofsays how far: every call before it is counted. It never passesperiod.end. - Final once it equals
period.end. For a calendar month that is normally a few minutes after 00:00 UTC on the 1st. - Nothing arrives late in a past hour. Usage is dated when it is recorded.
- One exception, a rebuild. When past hours are rebuilt from their records,
as_ofsteps back to the first of them until the rebuild is done, then comes forward again. Wait for it before you invoice.
Which keys you can read#
- Your workspace's keys only. A Full access key reads every key made in its workspace, in the console or through
/v1/keys, of any kind. A Manage device keys key reads the keys/v1/keyslists for it, the Decisions-only ones. - Any other id is
404not_found_error, "Not found.", another workspace's key, a key that does not exist and an id that is not one alike, never403, so a key id cannot be probed. - Revoked and expired keys stay readable, as their usage is kept indefinitely: billing a device after it is revoked is the usual case.
- Always open: an empty balance never makes these routes
402, and a suspended workspace can still read its usage, to bill its own customers.
Errors#
| Status | Type | When | What to do |
|---|---|---|---|
400 | api_usage_error | A query it cannot take, with one of: "start is an RFC 3339 time, such as 2026-10-01T00:00:00Z." (or end), "end is after start.", "A period is at most 400 days.", "granularity hour is for a period of at most 7 days; use day or month.", "granularity is none, hour, day or month.", "group_by is model or channel.", "start and end are times between the years 1 and 9999.", "Unknown query parameter: <name>.", "start and end are required.", "A label filter is name:value." (or "..., as labels are written."), "device_id is 1 to 128 printable ASCII characters, not starting with a space.", "key_id is a key's id, key_ and 32 hex digits.", "At most 100 key_id filters.", "scope is full, decide or keys.", "status is active, expired, revoked or all.", "include_empty is true or false.", "limit is a whole number from 1 to 1000.", "limit is at most 100 with a granularity other than none.", "cursor is a next_cursor this list gave." | Fix the query |
401 | authentication_error | The key is missing, unknown or revoked, or has expired (details.reason key_expired) | Check the Authorization header |
403 | key_scope_denied | A Decisions-only key, "This key may only make decisions.", or a Publishable key, "A publishable key may only make decisions." | Read usage from your server, with a Full access or Manage device keys key |
429 | rate_limit_error | More than 10 usage reads a second for your workspace, both routes together: "Rate limit reached. Retry after 1 s.", with Retry-After and retry-after-ms | Wait Retry-After, then retry |
529 | overloaded_error | Usage cannot be read right now, or this read waited more than a second behind your workspace's others, with Retry-After | Read one page at a time; wait Retry-After, then retry |
Limits#
- Their own rate: 10 reads a second per workspace, both routes together, apart from your rate limits: reading usage never slows your decisions, and decisions never slow it. Over it,
429rate_limit_errorwithRetry-After. - One read at a time: your workspace's usage reads are answered one after another, so reads sent in parallel queue, and one that waits more than a second is
529. Read pages in turn. - One call or many: read a month's invoice run with pages of
/v1/usage, not a call per key. - Never cached: every answer carries
Cache-Control: private, no-store, andx-decisionnode-request-idas every call does.