Skip to main content

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/current returns the session: the user, their tenant, their role and the permissions it resolves to.
  • POST /api/v1/auth/logout ends it — on the platform and at the identity provider.
  • A user can hold sessions on several devices; GET /api/v1/auth/sessions lists them and DELETE /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.

  1. Generate a PKCE verifier and its S256 challenge, and a random state.
  2. Listen on a free port on 127.0.0.1.
  3. POST /api/v1/auth/discover with "platform": "cli", the challenge, the state, and "redirectUri": "http://127.0.0.1:<port>/callback". It returns the authorizeUrl to open.
  4. Open authorizeUrl in the person's browser. Once they have signed in, the browser arrives at your /callback with a code and a state. Accept it only if the state is the one you sent, and send the browser on to the doneUrl that discover returned, a page saying they are signed in.
  5. POST /api/v1/auth/oidc/exchange with the code, your verifier, "platform": "cli" and the same redirectUri. The response is the session, with its token in sessionToken.

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:

  1. Sign a JWT with the key file's private key: RS256, header kid = the file's keyId, claims iss and sub = its userId, aud = the identity provider's address (authorization_servers[0], below), not its token endpoint, iat = now and exp a few minutes later. Any JWT library can sign it.

  2. Exchange it at the identity provider's token endpoint (the token_endpoint in <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_token lives 12 hours (its expires_in).

  3. 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).

See Booking and Tags.

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.