Authentication
Two credentials reach the API. A session belongs to a person who signed in; an API key belongs to a system, and signs in to a session of its own. Either way, what a call may do is a role's permissions in one tenant.
Sessions
A signed-in user holds one session, represented by an opaque token. There are no JWTs to decode and nothing to verify on your side; the token is a handle the API resolves on every request.
- Apps and your own clients send it as
Authorization: Bearer <token>. - The portal holds it in an HttpOnly cookie set by the API; a browser front end on the portal's origin never touches the token.
GET /api/v1/auth/sessions/currentreturns the session: the user, their tenant, their role and the permissions it resolves to.POST /api/v1/auth/logoutends it — on the platform and at the identity provider.- A user can hold sessions on several devices;
GET /api/v1/auth/sessionslists them andDELETE /api/v1/auth/sessions/{id}ends one.
Web, cli and MCP sessions expire after 12 hours without activity and 24 hours regardless, and an
MCP session also when its access token does; mobile sessions expire after 90 days without activity.
A 401 unauthorized means the session is gone — sign in again.
Signing in
Credentials never pass through the API. Every sign-in happens on the identity provider's
page — password, passkey, or a vendor account such as Apple, Google or Microsoft — and the API
meets the person on the way back. You register nothing: where the provider sends the person back
is fixed by the kind of client, which each call names as its platform.
The portal sends the person to GET /api/v1/auth/oidc/start. The provider signs them in and
returns them to GET /api/v1/auth/oidc/callback, which sets the session cookie and redirects into
the portal.
The platform's own apps sign in as ios or android, and the provider returns them to the
apps' fixed app://auth/callback.
Your own code signs in as cli, the way a command-line tool does: it listens on a port of
the machine it runs on, and the provider sends the browser back to that port.
- Generate a PKCE verifier and its S256 challenge, and a random
state. - Listen on a free port on
127.0.0.1. POST /api/v1/auth/discoverwith"platform": "cli", the challenge, thestate, and"redirectUri": "http://127.0.0.1:<port>/callback". It returns theauthorizeUrlto open.- Open
authorizeUrlin the person's browser. Once they have signed in, the browser arrives at your/callbackwith acodeand astate. Accept it only if thestateis the one you sent, and send the browser on to thedoneUrlthatdiscoverreturned, a page saying they are signed in. POST /api/v1/auth/oidc/exchangewith thecode, your verifier,"platform": "cli"and the sameredirectUri. The response is the session, with its token insessionToken.
A web front end served from its own address can't sign in: only the portal, the platform's apps
and cli clients can.
Identities
A person is one identity across every tenant they belong to, and a user in each. The identity holds no credential: passwords, passkeys, second factors and vendor sign-ins all live with the identity provider, which the API meets at sign-in. Which provider organisation a person's account lives in is fixed when it is made.
Staff see a person's sessions (userSessions) and end them, and can send them a way to sign in
or release a sign-in locked after too many wrong passwords (userSignIn). A person's own sign-in
methods and second factors are theirs to change, under /auth/security, and every such change
needs a sign-in or a verification within the last few minutes (reauthentication_required).
Tenants and roles
A person can hold a user in more than one tenant; a session is in one tenant at a time.
PATCH /api/v1/auth/sessions/current with a tenantId moves it. What a session may do is its
role's permissions — participant, coach, admin, platform admin or sales, or API client for an
API key — and staff reach every participant in the tenant the session is in. A
403 forbidden is the role; a 403 consent_required is a participant whose health data cannot
be written until they consent.
A role can require a second factor. When it does and the sign-in did not use one, the session
holds no permissions: every resource returns 403, the session reports needsSecondFactor, and
only the user's own security settings are reachable, until they enrol a factor in that session or
sign in again with one. Participants are never required to have one.
Real-time messages
WebSocket connections cannot carry an Authorization header, so exchange the session for a
single-use ticket first:
POST /api/v1/auth/wsToken
It lives for thirty seconds and is used once, as wss://<api-host>/ws?token=<ticket>. The
connection dies with the session.
API keys
For a system calling the API on its own behalf — a booking provider, an HR system writing attributes back — a platform admin creates an API key, choosing its role and when it expires (at most two years ahead), and downloads its key file. The file is shown once.
A key signs in the way people do: at the identity provider, into a session. It acts as its own service account, a user in the tenant named after the key, so what it may do is its role's permissions, the audit trail names it, and it reaches nothing outside its tenant.
To call the API with it:
-
Sign a JWT with the key file's private key:
RS256, headerkid= the file'skeyId, claimsissandsub= itsuserId,aud= the identity provider's address (authorization_servers[0], below), not its token endpoint,iat= now andexpa few minutes later. Any JWT library can sign it. -
Exchange it at the identity provider's token endpoint (the
token_endpointin<identity provider's address>/.well-known/openid-configuration) with the OAuth JWT bearer grant (RFC 7523):curl -X POST "<token_endpoint>" \--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \--data-urlencode "scope=openid urn:zitadel:iam:org:project:id:<API project id>:aud" \--data-urlencode "assertion=<signed JWT>"The response's
access_tokenlives 12 hours (itsexpires_in). -
Send it as
Authorization: Bearer <access token>.
A token without that scope is refused. The API publishes both values at
GET /.well-known/oauth-protected-resource/api/v1 (RFC 9728): the identity provider's address is
authorization_servers[0], and the scope to ask for is in scopes_supported. Reuse a token until
it expires; when a call returns 401 unauthorized, get a new one and retry once.
The API client role reads the tenant's catalog — atoms, sequences, journeys, surfaces, tags, groups, extras, cohorts and task types — and does two things to users:
- redeems a one-time booking token for the user it was minted for;
- writes tags to a user by the tags' external ids (attribute write-back).
It reaches nothing else of a user's. Endpoints that act on a signed-in person's own session or
account, such as GET /api/v1/auth/sessions/current, refuse a key with 403 forbidden. To rotate
a key, create a new one, switch to it, then delete the old one; deleting a key stops it at once.
Each key reports when it was last used.