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.

AreaScope✋Grants
Tenant settingstenant:readRead 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)
Casescase:readGet/list cases, status, messages, income bands, the case events stream
case:write✋Create cases, decisions, messages, replay, complete, mint embed tokens
Documentsdocument:readDownload original files, search, export
document:write✋Upload documents (direct, presigned, from-source, packet)
Agentsagent:readGet/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 typesdoctype:readGet/list document types and versions
doctype:write✋Create/discover/generate/clone/promote/update/lock document types
doctype:delete✋Delete document types
Runsexecution:readExecutions, intake status/artifacts, playground history, batch job status
Reportsreport:readCase report, case outputs and output downloads
Toolstool:invoke✋Invoke tools (v1, v2) and create batch jobs
Webhookswebhook:readList 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.

MethodPathWhoPurpose
GET/v1/keysadmin, memberList my keys (?status= filters)
POST/v1/keysadmin, memberCreate a key; a member's ✋ key is pending_approval
GET/v1/keys/{key_id}admin, memberGet one of my keys
POST/v1/keys/{key_id}/rotateadmin, memberRotate my key
POST/v1/keys/{key_id}/revokeadmin, memberRevoke my key (withdraws a pending request)
DELETE/v1/keys/{key_id}admin, memberDelete my revoked, expired or rejected key
GET/v1/admin/keysadminList all keys (?status=pending_approval is the approval queue; ?owner_id= filters)
POST/v1/admin/keysadminCreate an org-owned key
POST/v1/admin/keys/{key_id}/rotateadminRotate any key
POST/v1/admin/keys/{key_id}/approveadminApprove a pending key
POST/v1/admin/keys/{key_id}/rejectadminReject a pending key ({"reason": "…"} required)
POST/v1/admin/keys/{key_id}/revokeadminRevoke any key
DELETE/v1/admin/keys/{key_id}adminDelete a revoked, expired or rejected key

Create API Key

POST /v1/admin/keysendpoint

Org admins only. Creates an org-owned key that works immediately.

Request body:

namestringrequired

Human-readable label for the key (1–100 characters, e.g. "Production Integration").

scopesstring[]required

Catalog scopes to grant, e.g. ["case:read", "case:write", "document:write"].

environmentstring

"live" (default) or "test".

expires_in_daysinteger

30, 60, 90 (default), 180 or 365.

reasonstring

Why 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/keysendpoint

Org admins only. Members list their own keys with GET /v1/keys.

Query parameters:

statusstring

active, expiring, rotating, expired, revoked, rejected or pending_approval.

owner_idstring

Only 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}/revokeendpoint

Org admins only. Members revoke their own keys with POST /v1/keys/{key_id}/revoke.

Path parameters:

key_idstringrequired

UUID 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

StatusDetail
401 Missing X-Api-Key headerNo key provided
401 API key invalidKey not found, or the key has been revoked
401 API key expiredKey is past its expires_at
401 API key pending approvalA member's key is waiting for an org admin's approval
401 API key rejectedAn org admin rejected the key request
403 Insufficient permissions: requires '<scope>' scopeKey lacks the required scope for the endpoint