Read what one key did and cost: a device's month so far, a day by the hour, a customer's year by the month. To read many keys at once, for an invoice run, use GET /v1/usage. How to bill from it is on Bill your customers per device.
https://api.decisionnode.com /v1/keys/{key_id}/usagecurl -G https://api.decisionnode.com/v1/keys/key_3b8f0d6e2a914c7b9e5f1a2c4d6e8f01/usage \
-H "Authorization: Bearer $DECISIONNODE_MANAGE_KEY" \
--data-urlencode "start=2026-10-01T00:00:00Z" \
--data-urlencode "end=2026-10-03T00:00:00Z" \
--data-urlencode "granularity=day"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."
Path parameters#
key_idstringrequired- The key:
key_and 32 hex digits, as/v1/keysand the console show it.
Query parameters#
startstring- RFC 3339, with any offset, read in UTC and rounded down to the hour: the first moment counted. Left out, 00:00 UTC on the 1st of this month.
endstring- 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. Left out, now, rounded up to the next hour. 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.
Response#
200 OK with the key, the period read, how current the figures are, the totals, and the buckets or splits you asked for.
{
"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
},
"period": {
"start": "2026-10-01T00:00:00Z",
"end": "2026-10-03T00:00:00Z"
},
"as_of": "2026-10-03T00:00:00Z",
"totals": {
"requests": 798,
"free_tier_requests": 0,
"input_tokens": 4364000,
"image_tokens": 3167000,
"video_tokens": 0,
"booster_calls": 0,
"cost": { "usd": 0.183288, "model_usd": 0.183288, "boosters_usd": 0 }
},
"buckets": [
{
"start": "2026-10-01T00:00:00Z",
"end": "2026-10-02T00:00:00Z",
"totals": {
"requests": 410,
"free_tier_requests": 0,
"input_tokens": 2255000,
"image_tokens": 1640000,
"video_tokens": 0,
"booster_calls": 0,
"cost": { "usd": 0.09471, "model_usd": 0.09471, "boosters_usd": 0 }
}
},
{
"start": "2026-10-02T00:00:00Z",
"end": "2026-10-03T00:00:00Z",
"totals": {
"requests": 388,
"free_tier_requests": 0,
"input_tokens": 2109000,
"image_tokens": 1527000,
"video_tokens": 0,
"booster_calls": 0,
"cost": { "usd": 0.088578, "model_usd": 0.088578, "boosters_usd": 0 }
}
}
]
}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.
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. totalsobject- The period's totals.
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.
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>." | 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 |
404 | not_found_error | "Not found." No key with this id that your key may read, or an id that is not one | Check the id, and that it is a key of this workspace |
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. - Never cached: every answer carries
Cache-Control: private, no-store, andx-decisionnode-request-idas every call does.