Skip to main content
Four credential families, four surfaces. A credential from one never opens another’s routes — that is enforced by the prefix, before any database lookup. Full detail in Credentials.

Staff sessions

Get one from sign-up, sign-in, or the two-factor exchange:
Two-factor accounts answer this instead, and no session is issued yet:
Branch on two_factor_required, then exchange the challenge at /v1/auth/two-factor within 5 minutes. See Two-factor. Then send it on every request:
Sessions live 30 days and are revocable: signing out, changing the password and resetting the password all kill them server-side. Losing one membership does not — it costs you that organization (403 not_a_member) and leaves the session good for the others. Losing the LAST one does not end it either. Signing in succeeds with active_organization_id null and organizations empty, and the session reaches exactly four routes: GET /v1/session, GET /v1/organizations, POST /v1/organizations and POST /v1/session/logout. Everything else answers 403 not_a_member. Branch on active_organization_code before routing anybody into an organization.

API keys

Server-side only. Created in the panel, shown once.
The key’s environment is resolved from its stored row on every request. Call /v1/me once at start-up to confirm which one you are holding.

Publishable keys

Not a header. It goes in the body of the endpoints that accept it:
The gate is not CORS — those paths answer any origin, deliberately. The gate is the key’s allowed-origins list. Empty means any origin; non-empty means exact matches only, and a missing Origin header is refused.

Contact sessions

Minted by boot, a redeemed magic link or an email code. Reads that contact and nothing else.
verified on the session says whether the identity behind it was proven. An unverified session is usable and marked, but barred from anything another person’s data could leak through.

Failures

Missing, malformed, expired or revoked credential, and nothing else. It means the credential is gone, so discarding it and signing in again is the right reaction.
The session is valid; the organization named in X-Organization-Id is not one you belong to. Same answer for somebody else’s organization, for one that does not exist and for a malformed code — distinguishing them would be a directory of our tenants. The repair is to change organization, never to sign out: the session is still good for the ones you are in.
Authenticated and a member, but your role does not carry the permission. Different from 401: signing in again will not help.
Also the answer for a resource in another organization or another environment. The 404 never reveals which.

Bringing the wrong family

A well-formed credential from another family is told so, rather than getting a flat “invalid”:
401
One is a config mistake, the other is a security event. They should not read the same in your logs.

Not revealing whether an account exists

Several endpoints answer identically whether or not an address is known. This is deliberate and worth preserving if you touch these paths: Without the timing equalization only real accounts would pay bcrypt’s tens of milliseconds, and latency alone would answer the question the status code refuses to.