ZitadelDocs

Submit step data and advance

Submits user input for the current step. The server validates, processes (e.g., verifies a credential), advances the state machine through any invisible steps, and returns the next visible step. The response sets an updated encrypted `HttpOnly` cookie (`_zflow`) with the new flow state. The server is stateless — all orchestration state is carried in this cookie between requests. **Important:** The `id` in the response may differ from the `id` used in the request. This happens when a flow pivots (pushes a new flow onto the stack) or when a stacked flow completes (auto-pops to the parent flow). Always use the `id` from the latest response for the next request. ## Flow completion When `step.type` is `complete`, the flow is terminal. The `step.behavior` field tells the frontend what to do: | `behavior` | Action | |------------- |------------------------------------------------------------| | `redirect` | Navigate to `redirect_uri` (OIDC/SAML auth request done). | | `show` | Render the step as a success screen (e.g., registration). | A `complete` step is only returned when the **entire flow stack** is done. If a stacked flow (e.g., recovery pivoted from login) finishes, the server auto-pops to the parent flow and returns the parent's next step — the frontend never sees a `complete` for intermediate flows. ## External sign-in `{action: "sso", sso_provider_id, return_target}` on a step that offers `sso_providers` returns the engine-emitted `sso-redirect` step, whose `redirect_url` the frontend navigates to. - The flow state does not change, but `_zflow` is re-sealed like on every response, so its ten-minute window restarts at this submission; the external sign-in and the return must complete within it. - A second `Set-Cookie` line carries the browser-binding cookie the callback checks: `HttpOnly`, `Path=/`, `SameSite=Lax`. - On every host except http loopback it is `__Host-_zsso` with `Secure`. - When the request host is http loopback (local development, where Safari rejects `Secure`), it is `_zsso` with no `Secure`; the `__Host-` prefix is dropped because it requires `Secure`. A connection whose `client_id` is a `${{ NAME }}` reference has it filled from the project's variables. A provider the engine cannot start a sign-in with, including a reference with no variable behind it, re-renders the step with `error.sso_unavailable`.

POST
/flow/{id}/submit

Submits user input for the current step. The server validates, processes (e.g., verifies a credential), advances the state machine through any invisible steps, and returns the next visible step.

The response sets an updated encrypted HttpOnly cookie (_zflow) with the new flow state. The server is stateless — all orchestration state is carried in this cookie between requests.

Important: The id in the response may differ from the id used in the request. This happens when a flow pivots (pushes a new flow onto the stack) or when a stacked flow completes (auto-pops to the parent flow). Always use the id from the latest response for the next request.

Flow completion

When step.type is complete, the flow is terminal. The step.behavior field tells the frontend what to do:

behaviorAction
redirectNavigate to redirect_uri (OIDC/SAML auth request done).
showRender the step as a success screen (e.g., registration).

A complete step is only returned when the entire flow stack is done. If a stacked flow (e.g., recovery pivoted from login) finishes, the server auto-pops to the parent flow and returns the parent's next step — the frontend never sees a complete for intermediate flows.

External sign-in

{action: "sso", sso_provider_id, return_target} on a step that offers sso_providers returns the engine-emitted sso-redirect step, whose redirect_url the frontend navigates to.

  • The flow state does not change, but _zflow is re-sealed like on every response, so its ten-minute window restarts at this submission; the external sign-in and the return must complete within it.
  • A second Set-Cookie line carries the browser-binding cookie the callback checks: HttpOnly, Path=/, SameSite=Lax.
  • On every host except http loopback it is __Host-_zsso with Secure.
  • When the request host is http loopback (local development, where Safari rejects Secure), it is _zsso with no Secure; the __Host- prefix is dropped because it requires Secure.

A connection whose client_id is a ${{ NAME }} reference has it filled from the project's variables. A provider the engine cannot start a sign-in with, including a reference with no variable behind it, re-renders the step with error.sso_unavailable.

Path Parameters

id*string

Flow ID returned by POST /flow or the latest POST /flow/{id}/submit. Used to look up the correct flow state from the encrypted cookie, which can hold multiple concurrent flows. May change between responses when a flow pivot or pop occurs — always use the id from the latest response.

Header Parameters

Origin?string

Standard browser Origin header. When a step issues a passkey challenge, the server derives the WebAuthn relying-party id and the allowed origin from this value. An sso submission is bound to it as well: return_target must be on this origin, and the callback route is built under it (see External sign-in above). Browsers send it automatically on the fetch POST; no client action is required.

Formaturi
_zflow*string

Encrypted flow state cookie set by POST /flow or the previous POST /flow/{id}/submit. The cookie holds the sealed state of one flow; the {id} path parameter must name that flow, any other id is refused.

Browsers send this automatically; non-browser clients must capture the Set-Cookie header and resend it.

Request Body

application/json
  1. body
session_token?string

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

action*string

Which action to take. Must be a key from the step's actions dictionary:

  • A regular action (e.g., "submit", "register", "back")
  • The reserved value "sso" — triggers SSO redirect (requires sso_provider_id)
fields?

User input values. Keys match field names from the step's fields dictionary.

gate_proofs?

Solutions for security gates. Keys match gate names from the step's gates dictionary. The orchestrator collects these from <zl-captcha> component events.

challenge_response?

Response to a pending challenge on the step. Required when the step has a challenge property (e.g., after submitting a passkey action). The <zl-passkey> component populates this after the WebAuthn ceremony.

sso_provider_id?string

ID of the selected SSO provider (from sso_providers[].id). Required when action is "sso".

return_target?string

The page hosting the orchestrator, where the flow resumes after an external sign-in: the callback sends the browser here unchanged, and the page loads GET /flow/{id} to render the next step. The client puts the flow id in it; the orchestrator reads ?flow=<id> on the reload. Required when action is "sso". An absolute URL; the server parses it and keeps a fragment, so a hash-routed page returns to its route. Its origin must equal the request origin; any other origin is rejected and no sign-in is started.

Lengthlength <= 2048

Response Body

Advanced to next step. The id may have changed if a pivot or stack-pop occurred — always use the returned id for the next request.

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/string/submit' \  --cookie '_zflow=string' \  -H 'Content-Type: application/json' \  -d '{  "action": "string"}'
{  "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"}