ZitadelDocs

Start a new flow

Resolves a flow definition based on purpose + audience context and returns the first capability step. Creates a new session implicitly unless `session_id` is provided (for step-up / reauth on an existing session). The response contains an `id` field — the flow handle. Use it as the path parameter for all subsequent `/flow/{id}/submit` calls. The response also sets an encrypted `HttpOnly` cookie (`_zflow`) containing the flow's orchestration state (current step, collected data, history). The server is stateless between requests — all flow state lives in this cookie. The browser sends it automatically on subsequent requests.

POST
/flow

Resolves a flow definition based on purpose + audience context and returns the first capability step. Creates a new session implicitly unless session_id is provided (for step-up / reauth on an existing session).

The response contains an id field — the flow handle. Use it as the path parameter for all subsequent /flow/{id}/submit calls.

The response also sets an encrypted HttpOnly cookie (_zflow) containing the flow's orchestration state (current step, collected data, history). The server is stateless between requests — all flow state lives in this cookie. The browser sends it automatically on subsequent requests.

Request Body

application/json
  1. body
project_id*project-id
Match^[a-zA-Z0-9_-]+$
purpose*string
Value in"login""register""recovery""profiling""reauth""link_account"
flow_definition_name?string

Name of a specific flow definition to use. When omitted, the engine selects the best-matching definition based on purpose + audience context.

Match^[a-z][a-z0-9-]*$
dry_run?boolean

Set to true to simulate the flow without actually creating any user or session. Useful for testing and validation of flow definitions. This requires setting the flow_definition_name property as well.

Defaultfalse
schema_version?string

Semver version of the flow definition JSON Schema to use. When omitted, the latest version is used.

Match^\d+\.\d+\.\d+$
challenge_nonce?challenge-nonce

Origin-bound, single-use nonce obtained from POST /bootstrap/challenge. Binds the request to the browser that initiated the flow, preventing cross-origin replay.

session_id?string

Existing session to attach to (for step-up / reauth). Omit to let the flow create a new session implicitly.

auth_request_id?string

OIDC/SAML auth request ID (links flow to protocol request).

redirect_uri?string
Formaturi
hint?

Response Body

Flow started, first step returned.

application/json
  1. response
id*string

Flow handle. Use this as the path parameter for subsequent submit/event calls. May change between responses when a flow pivot or pop occurs.

session_id*string

Underlying session ID. Stable across all stacked flows.

session_token?string

Reserved for future rotation. The sealed _zflow cookie carries the flow state today.

step*

A step contains ordered capability arrays: what to collect (fields), what the user can do (actions), and what security gates must be satisfied (gates). The LiquidJS template in branding.liquid_template iterates these arrays in order and builds name-keyed indexes locally for lookup.

branding?

Resolved branding inherited from the app → team → project hierarchy. Determined at flow creation based on audience context. Does not change between steps.

redirect_uri?string

Present when step.complete is redirect.

Formaturi
handoff_token?string

Short-lived, audience-bound token produced on flow completion. Exchange it for a session via POST /sessions/exchange. Only present when step.complete is set.

handoff_token_expires_at?string

Expiry timestamp for handoff_token. Only present when handoff_token is set.

Formatdate-time
curl -X POST 'https://example.com/flow' \  -H 'Content-Type: application/json' \  -d '{  "project_id": "string",  "purpose": "login"}'
{  "id": "string",  "session_id": "string",  "session_token": "string",  "step": {    "name": "string",    "texts": {      "title_key": "string",      "description_key": "string"    },    "error": "string",    "complete": "redirect",    "redirect_url": "http://example.com",    "fields": [],    "identifier": {      "value": "string",      "autocomplete": "string"    },    "actions": [],    "gates": {},    "sso_providers": [],    "challenge": {      "method": "passkey",      "challenge_id": "string",      "options": {}    }  },  "branding": {    "$schema": "string",    "layout": "centered",    "liquid_template": "string",    "logo_url": "http://example.com",    "hero_url": "http://example.com",    "theme": {      "mode": "light",      "light": {        "logo_url": "http://example.com",        "palette": {          "primary": "#4F46E5",          "on_primary": "#4F46E5",          "background": "#4F46E5",          "surface": "#4F46E5",          "muted": "#4F46E5",          "border": "#4F46E5",          "text": "#4F46E5",          "text_muted": "#4F46E5",          "link": "#4F46E5",          "success": "#4F46E5",          "warning": "#4F46E5",          "error": "#4F46E5"        }      },      "dark": {        "logo_url": "http://example.com",        "palette": {          "primary": "#4F46E5",          "on_primary": "#4F46E5",          "background": "#4F46E5",          "surface": "#4F46E5",          "muted": "#4F46E5",          "border": "#4F46E5",          "text": "#4F46E5",          "text_muted": "#4F46E5",          "link": "#4F46E5",          "success": "#4F46E5",          "warning": "#4F46E5",          "error": "#4F46E5"        }      }    },    "typography": {      "font_family": "string",      "font_url": "http://example.com",      "scale": 0.75    },    "shape": {      "radius": "none",      "density": "compact",      "logo_scale": 0.5    }  },  "redirect_uri": "http://example.com",  "handoff_token": "string",  "handoff_token_expires_at": "2019-08-24T14:15:22Z"}