API authentication
This page explains how to authenticate to the platform API: which credentials exist, how to send them, how long they live, and how the platform decides — on every single request — what your credential may do.
Honest alpha note: there is no public API hostname yet. The web console at https://console.codyhill.dev is the public front door. Scripts and the CLI reach the API through the $CAI_API environment variable — an endpoint your administrator provides — or, with cluster access, platformctl sets up a port-forward automatically. The examples below assume export CAI_API=http://localhost:8080.
One header, four credential shapes
Every authenticated request sends the same header:
Authorization: Bearer <credential>
The platform accepts four shapes, dispatched by what the credential looks like:
| # | Credential | Shape | Lives | Revocable? | Use it for |
|---|---|---|---|---|---|
| 1 | API key | cai_<keyid>_<secret> | 0 (never) or 1–3650 days | Yes, instantly | Scripts, CI, anything unattended |
| 2 | Session token | Opaque signed token from login | 12 hours | No | Interactive work; what the console and platformctl login use |
| 3 | Automation token | Long-lived machine credential in a Kubernetes secret | Long-lived | Operator-managed | Cluster-level automation and e2e tests |
| 4 | Nothing | — | — | — | Health checks, invoking agents, invitation links, the embed widget |
1. API keys
An API key belongs to a person or a service account. Only a hash is stored server-side, and the secret is shown exactly once at creation. Verification checks the hash, then re-resolves the owner's authority from the database — a key carries identity, never a frozen set of permissions.
2. Session tokens
Sign in to get one:
curl -s $CAI_API/v1/auth/login \
-d '{"email":"you@example.com","password":"your-password"}'
You should see:
{"token":"...","email":"you@example.com","role":"user","must_change_password":false,"expires_at":1765480000}
Session tokens are signed and stateless: the server keeps no record of them, which is why they cannot be revoked before their 12-hour expiry.
Deleting a user or changing their password stops future logins — it does not kill a token already issued, which stays valid until it expires (at most 12 hours). Permission checks are live, though: demote or remove the account and the token's requests start failing immediately, because authority is re-read per request, not from the token.
Login is throttled: 10 attempts per IP and 50 per account per 15 minutes. Past that you get a 429 with a Retry-After header:
too many sign-in attempts; try again in <duration>
A wrong email and a wrong password get the same answer — invalid email or password — so accounts cannot be enumerated. Passwords must be at least 12 characters. An account still owing a forced password change gets this on every management route until it complies:
password change required before using this API
3. The automation token
Operators with cluster access have a root machine credential stored as a Kubernetes secret. The API's own 401 message tells you how to fetch it:
kubectl -n cai-agents get secret cai-automation-token \
-o jsonpath='{.data.token}' | base64 -d
It is used by the platform's test suites and as platformctl's last-resort credential. Treat it as cluster-root: prefer a service-account key for anything routine.
4. No credential at all
Anonymous requests reach exactly four surfaces:
GET /healthz- the data plane —
POST /v1/agents/{name}/invokeand its streaming variant are open by default so that apps can talk to agents without holding platform credentials (operators can close this withINVOKE_AUTH_REQUIRED=true; writing to memory always requires auth) - invitation preview and accept (the invitation token in the link is the credential)
- the public embed-widget routes (the
emb_key is the identity — see embedded chat)
Everything else — the management plane, anything that creates, changes, or inspects platform state — requires credential 1, 2, or 3.
An invalid or expired credential is treated as no credential: identity-optional routes still work, and management routes answer 401 with a message that includes its own fix:
this is a management endpoint and requires authentication. Sign in (POST /v1/auth/login) and send 'Authorization: Bearer <token>', or use the automation token: kubectl -n cai-agents get secret cai-automation-token -o jsonpath='{.data.token}' | base64 -d
Authority is re-resolved on every request
This is the property that makes the rest of the model safe: roles, project grants, org membership, and the platform-admin bit are re-read from the database on every request. The claims inside a token are hints, never the decision.
Concretely:
- Revoke a grant, and the very next request using any of that principal's credentials reflects it.
- An org admin demoted at 14:00 loses admin on every project in the org at 14:00, even mid-session.
- A personal API key minted while you were an admin does not keep you an admin.
Machine-credential fences
Two rules stop a leaked machine key from escalating:
- A service-account key can never create or manage credentials, accounts, or membership:
a service account cannot create or manage credentials. Sign in as a user (or use your own API key) to issue keys - otherwise a leaked key could mint replacements and revoking it would achieve nothing. - A workload key (shape
cai_wl_…, auto-issued to deployed workloads) can do exactly one thing — mint a short-lived read token for its own project's secrets. On any management route:this credential is a deployed workload's key; it can only mint a read token for its own project's secrets, not use management endpoints.
Endpoints that act on behalf of a person answer machine credentials with an explicit 403 rather than a misleading 401.
Rate limits and request ids
Beyond the login throttle, requests pass a per-principal token bucket keyed on the credential (falling back to per-IP for anonymous calls); operators tune it with RATELIMIT_RPS and RATELIMIT_BURST. Every response carries an X-Request-Id header, and every error uses one envelope:
{"error": "<message>", "request_id": "<id>"}
Quote the request_id when you contact support — it locates the exact log lines.
Summary
| Question | Answer |
|---|---|
| How do I authenticate? | Authorization: Bearer <credential> on every request |
| What should my CI use? | A service-account API key via $CAI_TOKEN |
| How long do sessions last? | 12 hours, not revocable — plan for it |
| Can a stale credential outlive a demotion? | No — authority is re-read per request |
| What needs no credential? | Health, agent invoke (by default), invitation links, embed widget |
| Where's the API? | $CAI_API / port-forward — no public hostname yet (alpha) |
Next steps
- Service accounts and API keys — minting and revoking keys in the console
- Security overview — what an authenticated principal is actually allowed to do
- Platform API reference