API keys, RBAC scopes, and key management for the 9thSense platform

Authentication


Every request to the 9thSense API must carry an API key in the X-Api-Key header. The platform uses a split-key scheme that enables fast database lookups without storing secrets in plaintext.

Key format

All new keys follow the pattern zk_<prefix>_<secret>:

  • zk — fixed sentinel identifying a 9thSense key.
  • <prefix> — 12 hex characters (6 random bytes). Stored in plaintext in the database and used for an indexed lookup.
  • <secret> — 32 hex characters (16 random bytes). Never stored. The full key zk_<prefix>_<secret> is bcrypt-hashed and the hash is stored.

On each request, the middleware extracts the prefix, fetches the matching row by prefix, then calls bcrypt.checkpw(raw_key, stored_hash) to verify. Legacy keys (SHA-256 hash stored without a prefix) are still accepted for backward compatibility.

The raw key is returned once at creation time and cannot be retrieved again. Store it immediately.

Sending the key

curl https://api.9thsense.ai/v1/cases \
  -H "X-Api-Key: zk_a1b2c3d4e5f6_0123456789abcdef0123456789abcdef"
import httpx

client = httpx.Client(
    base_url="https://api.9thsense.ai",
    headers={"X-Api-Key": "zk_a1b2c3d4e5f6_0123456789abcdef0123456789abcdef"},
)

resp = client.get("/v1/cases")
resp.raise_for_status()

The middleware returns 401 Missing X-Api-Key header if the header is absent, and 401 API key invalid or 401 API key revoked if validation fails.

RBAC scopes

Every API key carries a list of scopes. Endpoints enforce the minimum required scope via the require_scope() dependency:

ScopePermitted operations
readGET endpoints — list cases, get executions, view analytics
writePOST/PUT/PATCH endpoints — create cases, upload documents, run pipelines
adminKey management — create, list, and revoke API keys

If your key lacks the required scope the API returns 403 Insufficient permissions: requires '<scope>' scope.

Scopes are additive. A production service key typically holds ["read", "write"]. An automation account that provisions keys needs ["read", "write", "admin"].

Creating a key

Requires the admin scope.

curl -X POST https://api.9thsense.ai/v1/admin/keys \
  -H "X-Api-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "acme-corp",
    "name": "production-backend",
    "scopes": ["read", "write"]
  }'

Response — 201 Created:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "tenant_id": "acme-corp",
  "name": "production-backend",
  "scopes": ["read", "write"],
  "raw_key": "zk_a1b2c3d4e5f6_0123456789abcdef0123456789abcdef",
  "revoked": false,
  "last_used": null,
  "created_at": "2026-03-25T09:00:00Z"
}

raw_key is the only time the secret is visible. Store it in your secrets manager immediately.

Listing keys

curl "https://api.9thsense.ai/v1/admin/keys?tenant_id=acme-corp" \
  -H "X-Api-Key: $ADMIN_KEY"

The response omits raw_key — only metadata is returned. The last_used timestamp is updated on every authenticated request (fire-and-forget, never blocks the response).

Revoking a key

curl -X DELETE https://api.9thsense.ai/v1/admin/keys/3fa85f64-5717-4562-b3fc-2c963f66afa6 \
  -H "X-Api-Key: $ADMIN_KEY"

Returns 204 No Content on success. Revoked keys are rejected immediately on the next request. The split-key lookup filters on revoked = FALSE, so a revoked prefix returns no row.

Open paths

The following paths never require authentication: /healthz, /docs, /openapi.json, /redoc, /metrics. All other paths enforce key validation when auth is enabled.