Every route takes a key of the kind Manage device keys (any other kind gets 403 key_scope_denied) and acts on the Decisions-only keys of its workspace. Make that key in the console; keep it on your server. How the kinds differ is on API keys, and the whole flow on Keys on devices.
| Method and path | What it does |
|---|---|
POST /v1/keys | Make a Decisions-only key. 201 with the key and its secret |
GET /v1/keys | The keys, newest first: label=name:value (repeat it; all must match), device_id and status (active, the default, expired, revoked or all) filter, limit 1 to 200 (50 by default) and cursor |
PATCH /v1/keys/{id} | Change its name, labels, limits, daily_spend_cap, expires_at or may_search_collections: see Change a key |
DELETE /v1/keys/{id} | Revoke it, at once: {"id", "revoked": true} |
POST /v1/keys/revoke | Revoke every active key whose labels match labels, at most 10,000 a call |
Make a key#
{
"name": "Kiosk 0412",
"device_id": "kiosk-0412",
"labels": { "store": "118", "fw": "2.4" },
"limits": { "requests_per_second": 5, "tokens_per_second": 20000 },
"daily_spend_cap": { "usd": 2 },
"expires_at": "2027-10-13T00:00:00Z",
"may_search_collections": false
}Request body
namestringrequired- 1 to 48 letters, digits, spaces, dots, dashes and underscores, unique among the workspace's active keys.
device_idstring- Your id for the device, 1 to 128 printable ASCII characters, not starting with a space, unique among the workspace's active keys.
labelsobject- Up to 16 names and values: a name of 1 to 32 characters of
a-z,0-9and_ . -, a value of 1 to 64 characters. limitsobjectrequests_per_secondandtokens_per_second, each a whole number from 1 to the workspace's own; left out ornull, the workspace's limits apply.daily_spend_capobject{"usd"}, to two decimals and at most USD 1,000; left out, no cap.expires_atstring- RFC 3339, at least a minute and at most 10 years ahead; left out, the key never expires.
may_search_collectionsbooleandefaultfalse- Allow
face_recognition.
{
"key": {
"id": "key_7c2e9a41b0d34f6e8a5c1d2b3e4f5a60",
"name": "Kiosk 0412",
"prefix": "dn_live_",
"scope": "decide",
"last4": "9f3a",
"device_id": "kiosk-0412",
"labels": { "store": "118", "fw": "2.4" },
"limits": { "requests_per_second": 5, "tokens_per_second": 20000 },
"daily_spend_cap": { "usd": 2 },
"spent_today": { "usd": 0 },
"expires_at": "2027-10-13T00:00:00Z",
"may_search_collections": false,
"status": "active",
"created_by_key": { "id": "key_1f0e2d3c4b5a69788796a5b4c3d2e1f0" },
"created_at": "2026-10-20T09:12:04Z",
"last_used_at": null,
"revoked_at": null
},
"secret": "dn_live_<64 hex digits>"
}secretis shown once, in this answer. A retry with the sameIdempotency-Keyheader and body within 24 hours returns the same key, as it is now, and its secret again, and makes no second key.- Only to the key that made it. A workspace's
Idempotency-Keyvalues are one namespace: another Manage device keys key sending the same one gets409idempotency_key_reused, as does the same key with another body, so give each provisioning server its own prefix. While the first call still runs, a repeat is409idempotency_in_progresswithRetry-After: wait it out and send the same call again, with the sameIdempotency-Key, never a new one. - Not once revoked. If the key has been revoked since, or its secret can no longer be shown, the replay is
409key_not_replayable: "The key this Idempotency-Key made is revoked or its secret can no longer be shown: revoke it if it is not, then create a new key with a new Idempotency-Key." - Rate: a workspace's Manage device keys keys make at most 20 keys a second, in bursts of up to 200, and 50,000 a day; beyond that,
429rate_limit_errorwithRetry-After. Keys made in the console count apart.
The key object#
{
"id": "key_7c2e9a41b0d34f6e8a5c1d2b3e4f5a60",
"name": "Kiosk 0412",
"prefix": "dn_live_",
"scope": "decide",
"last4": "9f3a",
"device_id": "kiosk-0412",
"labels": { "store": "118", "fw": "2.4" },
"limits": { "requests_per_second": 5, "tokens_per_second": 20000 },
"daily_spend_cap": { "usd": 2 },
"spent_today": { "usd": 0 },
"expires_at": "2027-10-13T00:00:00Z",
"may_search_collections": false,
"status": "active",
"created_by_key": { "id": "key_1f0e2d3c4b5a69788796a5b4c3d2e1f0" },
"created_at": "2026-10-20T09:12:04Z",
"last_used_at": null,
"revoked_at": null
}idstringkey_and 32 hex digits.namestring- 1 to 48 letters, digits, spaces, dots, dashes and underscores, unique among the workspace's active keys.
prefixstringdn_live_, the start of the secret.scopestringdecidefor every key/v1/keysmakes; the console's kinds arefull,keysandpublishable.last4string- The secret's last four characters. The secret itself is returned once, when the key is made.
device_idstring or null- Your id for the device, 1 to 128 printable ASCII characters, not starting with a space, unique among the workspace's active keys.
labelsobject- Up to 16 names and values: a name of 1 to 32 characters of
a-z,0-9and_ . -, a value of 1 to 64 characters. limitsobject or null- The key's own rate limits, a second;
nullwhen the workspace's limits apply.requests_per_secondinteger or null- Requests a second, a whole number from 1 to the workspace's own.
tokens_per_secondinteger or null- Input tokens a second, a whole number from 1 to the workspace's own.
daily_spend_capobject or null{"usd"}, the most the key may spend in a day, to two decimals and at most USD 1,000;nullfor none. Resets at 00:00 UTC.spent_todayobject or null{"usd"}, what the key has spent since 00:00 UTC;nullon a key without a cap.expires_atstring or null- When the key stops working, RFC 3339, at least a minute and at most 10 years ahead;
nullfor never. A Full access key's expiry is checked when a session's socket connects, and a socket already open keeps going until its session ends. may_search_collectionsbooleandefaultfalse- Whether the key may run
face_recognition. statusstringactive,expiredorrevoked.created_by_keyobject or null{"id"}, the Manage device keys key that made it through/v1/keys;nullfor one made in the console.created_at, last_used_at, revoked_atstring or null- RFC 3339.
List keys#
GET / answers {"items": [Key], "next_cursor"}; pass next_cursor as cursor for the next page, until it is null. Only Decisions-only keys are listed; a secret never is. Any other query parameter is 400 api_usage_error, "Unknown query parameter: <name>." There is no call that reads one key by its id: find a device's key with GET / (add status=all for one revoked or expired), or keep the key that POST /v1/keys and PATCH answer.
Change a key#
PATCH /v1/keys/{id}takesname,labels,limits,daily_spend_cap,expires_atandmay_search_collections. A field left out stays as it is;labelsis replaced whole, andnullclearslabels,limits,daily_spend_caporexpires_at. It answers the key.device_idnever changes: sending it is a422at["body", "device_id"], "A key's device_id is set when it is made: revoke it and make another." Any other field is a422extra_forbidden, and an empty body a400, "Nothing to change."- An expired key takes only a rename or a revoke: every other change is
409key_expired, "This key has expired: make a new one. An expired key is never brought back." A revoked key is409already_revoked.
Revoke#
- One key:
DELETE /v1/keys/{id}answers{"id", "revoked": true}. A key already revoked is409already_revoked. - A group:
POST /v1/keys/revokewithlabels, a map every key must match, and optionallydevice_ids, up to 1,000 the keys must be among. It answers{"revoked": <count>, "more": <boolean>}, at most 10,000 a call.moreistruewhile matching keys are left, which can happen with fewer than 10,000 when a key was in use at that moment: call it again untilmoreisfalse. With"dry_run": trueit revokes nothing and answers{"would_revoke": <count>, "more": <boolean>}, the count up to 10,000 andmorewhen more keys match;dry_runmust be a JSON boolean (a string such as"true"is a422bool_type, never read asfalse). - At once: the next call with a revoked key gets
401. A revocation cannot be undone.
Errors#
| Status | Type | When |
|---|---|---|
401 | authentication_error | The key is missing, unknown, expired (details.reason key_expired) or revoked |
403 | key_scope_denied | The key is not a Manage device keys key: a Full access key, "Keys are managed with a keys key, made in the console."; a Decisions-only key, "This key may only make decisions." |
404 | not_found_error | "Not found." No such key among the workspace's Decisions-only keys |
400 | api_usage_error | An unknown query parameter on the list; an empty PATCH body, "Nothing to change." |
409 | already_revoked | The key is already revoked |
409 | idempotency_key_reused | "This Idempotency-Key was already used with a different request." Another body, or another Manage device keys key of the workspace, with an Idempotency-Key already used |
409 | idempotency_in_progress | "A request with this Idempotency-Key is still running. Try again shortly." With Retry-After: wait, then send the same call with the same Idempotency-Key |
409 | key_not_replayable | A replayed create whose key is revoked or whose secret can no longer be shown: "The key this Idempotency-Key made is revoked or its secret can no longer be shown: revoke it if it is not, then create a new key with a new Idempotency-Key." |
409 | key_expired | A change to an expired key other than a rename: "This key has expired: make a new one. An expired key is never brought back." |
422 | a detail list | A field out of bounds: labels past their limits, an expires_at less than a minute or more than 10 years ahead, a cap over USD 1,000, a name already used (name_taken), a device_id already on an active key (device_id_taken), a device_id in a PATCH, a field the route does not take (extra_forbidden), a dry_run that is not a boolean (bool_type) |
429 | rate_limit_error | Keys made faster than the rate above: "Keys are being made faster than this workspace may: 20 a second, bursts of 200, 50,000 a day.", details.reason key_creation_rate, with Retry-After |
529 | overloaded_error | Keys cannot be made right now, with Retry-After: send the same call, with the same Idempotency-Key, after it |
The errors a Decisions-only key can meet on a decision, 403 key_scope_denied and 429 key_spend_cap_reached, are on Errors.