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, resetting the password and losing the membership all kill them server-side.

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, hosted sign-in, or email verification. 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 also “you are not a member of that organization”, because the membership JOIN matches nothing and distinguishing the two would leak that the organization exists.
Authenticated, 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.