Skip to main content

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.

Where the API lives

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:

#CredentialShapeLivesRevocable?Use it for
1API keycai_<keyid>_<secret>0 (never) or 1–3650 daysYes, instantlyScripts, CI, anything unattended
2Session tokenOpaque signed token from login12 hoursNoInteractive work; what the console and platformctl login use
3Automation tokenLong-lived machine credential in a Kubernetes secretLong-livedOperator-managedCluster-level automation and e2e tests
4NothingHealth 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.

Live tokens survive account changes

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 planePOST /v1/agents/{name}/invoke and its streaming variant are open by default so that apps can talk to agents without holding platform credentials (operators can close this with INVOKE_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

QuestionAnswer
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