zoxaAI
Homepage
API Reference

Authentication

API keys, header format, WebSocket auth via query string, and organization scoping.

zoxaAI uses API keys for server-to-server requests and JWTs for dashboard sessions. Most integrations use API keys.

API key format

zsk_<urlsafe-base64-32-bytes>

Generated server-side as f"zsk_{secrets.token_urlsafe(32)}". The full key is shown only at creation time — after that, only the first 8 characters (key_prefix) are visible in any list response.

HTTP authentication

Send the key on every request:

X-API-Key: zsk_a1b2c3d4...
curl https://dashboard.zoxa.ai/api/v1/agents \
  -H "X-API-Key: zsk_..."
await fetch("https://dashboard.zoxa.ai/api/v1/agents", {
  headers: { "X-API-Key": "zsk_..." },
});
import httpx
client = httpx.Client(
    base_url="https://dashboard.zoxa.ai/api/v1",
    headers={"X-API-Key": "zsk_..."},
)
client.get("/agents")

WebSocket authentication

Browser WebSocket handshakes can't carry custom headers, so the key goes in the query string:

wss://dashboard.zoxa.ai/api/v1/ws/audio/{callId}?api_key=zsk_...

Server-side WebSocket clients (Node.js ws, Python websockets, Go) may send X-API-Key as a custom header instead — both forms are accepted. The query-string form works everywhere.

Don't put the key in client-side JavaScript

Keep zsk_... keys server-side only — shipping one to end-users gives them full org control. Browser calls belong behind your own backend, which holds the key and brokers the call.

Dashboard JWT (alternative)

The dashboard itself authenticates with a JWT obtained from POST /api/v1/auth/login. Server-side integrations should never use this path — use an API key.

Authorization: Bearer eyJhbGciOi...

Organization scoping

Every key is bound to one organization at creation time. All requests made with that key operate within that organization — you never pass organization_id anywhere.

BehaviorRule
ScopeSingle org per key.
Cross-org accessImpossible. A key only reads/writes within its own org.
Multiple orgsCreate a separate key in each org.
ListingList endpoints automatically filter to the key's org.
MutationsResource ids you can't access return 404 (not 403) to avoid leaking ids across orgs.

Managing keys via API

API keys can be managed through the same API.

MethodPathPurpose
GET/api/v1/user/api-keysList active keys (full key value never returned). Add ?include_archived=true to include revoked keys.
POST/api/v1/user/api-keysCreate a new key. Response includes the full key — store it now.
DELETE/api/v1/user/api-keys/{api_key_id}Revoke a key. Effective immediately.
PUT/api/v1/user/api-keys/{api_key_id}/reactivateRestore a revoked key.

Full request/response details on the API Keys section.

Failure modes

StatusdetailWhen
401"Authorization header required"No X-API-Key header (or Authorization: Bearer ... for JWT).
401"Invalid or expired API key"Key not found, or has been revoked.
401"Invalid or expired token"JWT expired or signature invalid.
403"Access denied"Auth succeeded but the user lacks scope (e.g., non-superuser hitting superuser endpoints).

Best practices

PracticeWhy
One key per environmentSeparate prod / staging / CI keys so a leak is scoped.
Store in a secrets manager, never in source controlThe key gives full org control until revoked.
Rotate periodicallyCreate the new key, deploy it everywhere, then revoke the old one.
Use key_prefix for in-product displayAll list responses include key_prefix (zsk_a1b2...) safe to render in admin dashboards.
Revoke before deletingIf you suspect a leak, revoke immediately — even if you can't track down where it was used.

On this page