Authorization: Bearer <token>:
| Token shape | When to use | X-Account-ID required? |
|---|---|---|
sk_live_<account>_<rest> — long-lived API key | SDKs, MCP, CI, server-to-server | No — the account is in the prefix |
Zitadel JWT (eyJ…) | Hub UI / interactive admin sessions | Yes |
sk_live_ API key. Mint
one at Hub UI → Settings → API Keys.
X-Account-ID header needed. The backend resolves your
account from the sk_live_ prefix.
Hostnames
api.kataven.ai is decoupled from the dashboard — outages on hub.* don’t
affect programmatic access, and rate-limit / monitoring policies apply
independently.
Mint, list, revoke API keys
API key management is Hub UI / Zitadel-JWT only. A leakedsk_live_ cannot mint another sk_live_, by design — otherwise key
rotation wouldn’t be a real defense. The POST/PATCH/DELETE
operations on /api/v1/api-keys return 403 Forbidden for
sk_live_ callers; the SDK doesn’t even expose them.
In the Hub UI:
- Settings → API Keys → Create API key
- Pick a name (e.g.
ci-deploy-bot) and an expiry (Never, 1 year, 90 days, 30 days). Per-tenant policy may restrict the max expiry. - Click Create. The full token shows up once — copy it immediately. We don’t store the plaintext, only an HMAC-SHA256 hash with a server-side pepper.
- Use the token in
Authorization: Bearer ….
Two key types, two purposes
After the 2026-05-08 split, credentials live in two separate tables with two separate prefixes:| Type | Prefix | Where it goes | Backend table |
|---|---|---|---|
| API key | sk_live_ | server-side / SDK / MCP — full account access | api_keys |
| Widget key | pk_live_ | customer HTML embed (<script data-client-key="…">) — public identifier, NOT a credential | widget_keys |
sk_live_ API key — no
paired pk_live_ required. The widget key is purely an account
identifier the browser hands back to /api/v1/widget/config.
Both are managed at Settings → API Keys in the Hub UI. Widget key
CRUD is also available via the SDK (it’s a public identifier, so
minting one carries no security risk).
Per-account databases
Kataven is multi-tenant by database isolation, not by row-level filtering. Each account has its own PostgreSQL database. TheX-Account-ID header (or, for sk_live_ tokens, the prefix) tells the
API which one to connect to — there is no account_id column anywhere.
Account names are validated against a regex
(^[a-z0-9][a-z0-9_-]{0,62}$) — anything else is rejected at the edge.
The reserved account kataven-admin is rejected on regular endpoints
with 403. It’s used only for the operator console.
Public endpoints
A small set of endpoints intentionally requires no auth:GET /api/accountsandGET /api/accounts/validate/{account}— account discovery before any token exists.POST /api/v1/widget/auth/*andGET /api/v1/widget/config— called cross-origin from customer-page widgets, before any end-user has a Kataven session. These have their own per-key auth (pk_live_*/sk_live_*) — see Widget concepts.
Errors
| Status | Meaning |
|---|---|
400 | Malformed token, or missing X-Account-ID for legacy JWT auth. |
401 | Missing or invalid bearer token. |
403 | kataven-admin rejected, JWT account mismatch, or revoked key. |
404 | Resource not found in this account’s database. |
429 | Rate limit exceeded — see Rate limits. |

