API key format, scopes, and key management endpoints
Authentication
9thSense uses API key authentication. Every request (except open paths) must include the X-Api-Key header.
X-Api-Key: zk_live_a1b2c3d4e5f6_0123456789abcdef0123456789abcdef
Key Format
New keys follow the split-key pattern:
zk_<env>_<prefix>_<secret>
zk— fixed literal prefix<env>— the environment the key is bound to (e.g.live,test). Keys created before environments existed have no env segment:zk_<prefix>_<secret><prefix>— 12-character hex token stored in plaintext for fast index lookup<secret>— 32-character hex token; the full key is verified with bcrypt
This approach allows O(1) DB lookup by prefix while never storing the secret in recoverable form.
Legacy keys (SHA-256 hashed, no prefix) are still accepted for backward compatibility. New keys should always use the split-key format.
Scopes
Each key carries one or more scopes from a 19-scope catalog, and each endpoint requires one of them. ✋ marks the scopes for which a tenant member's key needs an org admin's approval.
| Area | Scope | ✋ | Grants |
|---|---|---|---|
| Tenant settings | tenant:read | Read settings (environments, retention, governance, notify/export config, AWS setup, embed origins) and analytics | |
tenant:write | ✋ | Change environments, retention, governance, report-notify, report S3 export | |
tenant:admin | ✋ | Manage embed origins (which sites may frame the app) | |
| Cases | case:read | Get/list cases, status, messages, income bands, the case events stream | |
case:write | ✋ | Create cases, decisions, messages, replay, complete, mint embed tokens | |
| Documents | document:read | Download original files, search, export | |
document:write | ✋ | Upload documents (direct, presigned, from-source, packet) | |
| Agents | agent:read | Get/list agents, versions, integrations, builder sessions | |
agent:write | ✋ | Create/update/clone/promote/rollback/deploy agents, integrations, builder | |
agent:delete | ✋ | Delete agents | |
agent:run | ✋ | Run agents/pipelines, submit intake | |
| Document types | doctype:read | Get/list document types and versions | |
doctype:write | ✋ | Create/discover/generate/clone/promote/update/lock document types | |
doctype:delete | ✋ | Delete document types | |
| Runs | execution:read | Executions, intake status/artifacts, playground history, batch job status | |
| Reports | report:read | Case report, case outputs and output downloads | |
| Tools | tool:invoke | ✋ | Invoke tools (v1, v2) and create batch jobs |
| Webhooks | webhook:read | List endpoints, deliveries, stats | |
webhook:write | ✋ | Create/delete endpoints, retry deliveries |
tenant:admin includes tenant:read and tenant:write. No other scope implies
another, so a key that both reads and creates cases needs
["case:read", "case:write"]. The legacy read, write and admin names are
rejected with 422 on new requests; existing keys were migrated to the
equivalent catalog scopes. Approval: members can request any scope; privileged (✋) ones, including tenant:admin, wait for an org admin. Until one approves, the key gets 401 API key pending approval. Requests pending 14 days are rejected automatically.
Key Management
Keys are managed from the dashboard, or with these routes using a dashboard
session (Authorization: Bearer <jwt>). They are role-gated, so an API key
calling them gets 403, whatever its scopes.
| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /v1/keys | admin, member | List my keys (?status= filters) |
POST | /v1/keys | admin, member | Create a key; a member's ✋ key is pending_approval |
GET | /v1/keys/{key_id} | admin, member | Get one of my keys |
POST | /v1/keys/{key_id}/rotate | admin, member | Rotate my key |
POST | /v1/keys/{key_id}/revoke | admin, member | Revoke my key (withdraws a pending request) |
DELETE | /v1/keys/{key_id} | admin, member | Delete my revoked, expired or rejected key |
GET | /v1/admin/keys | admin | List all keys (?status=pending_approval is the approval queue; ?owner_id= filters) |
POST | /v1/admin/keys | admin | Create an org-owned key |
POST | /v1/admin/keys/{key_id}/rotate | admin | Rotate any key |
POST | /v1/admin/keys/{key_id}/approve | admin | Approve a pending key |
POST | /v1/admin/keys/{key_id}/reject | admin | Reject a pending key ({"reason": "…"} required) |
POST | /v1/admin/keys/{key_id}/revoke | admin | Revoke any key |
DELETE | /v1/admin/keys/{key_id} | admin | Delete a revoked, expired or rejected key |
Create API Key
POST /v1/admin/keysendpointOrg admins only. Creates an org-owned key that works immediately.
Request body:
namestringrequiredHuman-readable label for the key (1–100 characters, e.g. "Production Integration").
scopesstring[]requiredCatalog scopes to grant, e.g. ["case:read", "case:write", "document:write"].
environmentstring"live" (default) or "test".
expires_in_daysinteger30, 60, 90 (default), 180 or 365.
reasonstringWhy the key is needed. Required on POST /v1/keys when the key needs approval, and stored only when approval is needed; max 500 characters.
Example request:
{
"name": "Production Integration",
"environment": "live",
"scopes": ["case:read", "case:write", "document:write"],
"expires_in_days": 90
}
Example response (201):
{
"key": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Production Integration",
"prefix": "a1b2c3d4e5f6",
"environment": "live",
"scopes": ["case:read", "case:write", "document:write"],
"status": "active",
"owner": null,
"created_at": "2026-03-25T10:00:00Z",
"expires_at": "2026-06-23T10:00:00Z",
"last_used_at": null,
"revoked_at": null,
"purge_at": null,
"approval": null
},
"secret": "zk_live_a1b2c3d4e5f6_0123456789abcdef0123456789abcdef"
}
The secret is returned only once, at creation (and at rotation). Store it securely — it cannot be retrieved again.
List API Keys
GET /v1/admin/keysendpointOrg admins only. Members list their own keys with GET /v1/keys.
Query parameters:
statusstringactive, expiring, rotating, expired, revoked, rejected or pending_approval.
owner_idstringOnly keys owned by this user (UUID).
Returns a JSON array of key resources (the key object above). The secret is never included.
Revoke API Key
POST /v1/admin/keys/{key_id}/revokeendpointOrg admins only. Members revoke their own keys with POST /v1/keys/{key_id}/revoke.
Path parameters:
key_idstringrequiredUUID of the key to revoke.
Returns 200 with the updated key ("status": "revoked"). Returns 404 if the key does not exist and 409 if it is already revoked. To remove a revoked key permanently, call DELETE /v1/admin/keys/{key_id} (204).
Error Responses
| Status | Detail |
|---|---|
401 Missing X-Api-Key header | No key provided |
401 API key invalid | Key not found, or the key has been revoked |
401 API key expired | Key is past its expires_at |
401 API key pending approval | A member's key is waiting for an org admin's approval |
401 API key rejected | An org admin rejected the key request |
403 Insufficient permissions: requires '<scope>' scope | Key lacks the required scope for the endpoint |