Skip to main content

User Onboarding

Overview

After account creation, users progress through a configurable series of onboarding steps. Each step represents a task the user must complete (or that can be skipped by platform configuration) before they are fully onboarded. The onboarding system is:
  • Step-based — a linear sequence of steps, each with a status
  • Configurable — steps can be enabled or disabled per-user by the platform
  • Idempotent — submitting the same step twice is a no-op, not an error
  • Event-sourced — every transition is recorded as an auditable event

Quick Start

1. Create an account

Onboarding starts automatically. The response includes the user profile (201 Created).

2. Check onboarding state

The response shows the current step, all steps with their status, and whether onboarding is complete.

3. Complete steps

For each step that requires user action, submit it when the user finishes:

4. Onboarding complete

When all steps are done, is_complete becomes true and current_step becomes "complete".

Onboarding State

Reading State

Fetch the current onboarding state from the dedicated resource:

Response Shape

The steps array only contains steps in the user’s cohort flow. A consumer (no org_id) and AGENT_CREATE2 user see five steps. A BYO user sees six (plus byo_safe); a USER_SIGNED_DEPLOY user sees six (plus safe_deploy). Clients should iterate the array rather than indexing into specific positions. The gated values above are those of an organisation user — the request carries x-juno-orgid, and no environment feature waives phone or KYC. A user without an org_id sees gated: true on all five. See Gating.

Step Statuses

Gating

gated answers whether a skipped status is possible for this user on this step — not whether it applies. A step with gated: false will always require action; a step with gated: true may arrive skipped on a later read if the platform’s configuration changes. Which configuration decides that depends on the user: Organisation users always complete phone and KYC verification: no environment feature waives either, so both report gated: false for them. The wallet steps (byo_safe, safe_deploy) are never waived within a cohort that has them, so they always report gated: false.

Step Metadata

Each step exposes a meta field. When a step has no per-user configuration to communicate, meta is always null — clients can treat meta: null as “no metadata for this step” rather than “metadata pending”.

Per-step meta shape

The byo_safe and safe_deploy entries only appear in the response for users in their respective cohorts. A consumer or AGENT_CREATE2 user will never see them in the steps array.

KYC Verification Meta

The kyc_verification step is where a user earns their Sigil (Proof of Personhood) — completing it is what unlocks the identity downstream steps read off.
  • websdk — use the full embedded verification SDK (default)
  • hybrid — use native API document upload with SDK-based liveness check only
  • document_only — use native API document upload with no liveness check or SDK required
The kyc_mode value is selected per integration server-side. Your frontend should read this value and render the appropriate verification flow. See the KYC guide for integration details, including how partner organisations can override the mode per environment.

State Machine

The diagram shows one presentation order shared by every cohort; a cohort’s flow is that order restricted to the steps it contains. Advancement moves to the first step that is required and not yet done, so any gated step that is disabled is passed over and recorded skipped — or the user reaches complete if nothing required remains. The two phone skipped transitions apply to users without an org_id, whose skips come from per-step platform kill switches. An organisation user’s skips come from their environment features, and none of those waives phone or KYC — so for them both steps are always walked. See Gating.

Wallet cohort: byo_safe vs safe_deploy

Every onboarded user gets a Safe smart wallet, but how the Safe gets there depends on which SIS environment features are enabled for the user’s organisation. The byo_safe and safe_deploy steps are mutually exclusive: a user’s cohort contains at most one of them, and the other does not appear in steps at all. Neither is reported skipped — a step a user’s cohort never had is absent, not waived. Users without an org_id have neither step in their flow — they’re outside the SIS-managed cohort, and their Safe is deployed for them.

Submitting the Safe step

The Safe step uses one polymorphic endpoint that branches on the user’s cohort:
GET returns the user’s mode (byo, user_signed_deploy, or agent_create2) along with a cohort-specific config block and the persisted wallet once available. config is always present and its shape matches mode; inner fields may be null when no work is currently prepared (e.g. a user_signed_deploy config returns user_operation: null, user_op_hash: null, predicted_safe_address: null until preparation runs). The POST body is a shape-only payload — the cohort is derived server-side from the user’s SIS environment, so the body does not carry a mode field. Submitting a payload whose shape doesn’t match the server-resolved cohort returns 409 (see Cohort mismatch below).

mode=byo

Sumvin verifies on-chain that the address is a Safe owned by the user’s primary EOA, persists the wallet in completed, and auto-advances onboarding. Resubmission with a different address replaces the previously-submitted Safe — the previous row is soft-deleted (deleted_at set, is_primary cleared) so the audit trail is preserved. Submitting off the step is answered by position, and the two directions are deliberately different statuses:

mode=user_signed_deploy

GET returns the prepared sponsored UserOp envelope when the step is current and no Safe is persisted yet (config.user_operation, config.user_op_hash, config.predicted_safe_address). When the step has not yet been prepared, the same config envelope is returned with those inner fields set to null:
Once prepared, the user signs the hash with their EOA and replays it to POST:
Sumvin forwards the signed UserOp to the bundler and persists a wallet record immediately in a provisional state so downstream features (like DID minting) can target the predicted address. Bundler inclusion confirms the wallet record via the status route below; a revert marks it failed. After submission, clients poll the on-chain UserOp status. While a UserOp is in flight, both the GET and POST responses surface a _links.user_op_status HAL link pointing to /v0/safe/rpc/{user_op_hash}/status — clients should consume that link rather than constructing the URL manually. See Submitting UserOperations for the full polling protocol — note especially the 503 + Retry-After semantics when finalisation hits a transient downstream error.

mode=agent_create2

The agent signer workflow creates the Safe automatically; the user has nothing to submit. GET returns the predicted Safe address once the agent signer is provisioned. POST is a uniform 204 No Content no-op so clients can use a single “submit” pattern across all cohorts:
Clients should not parse the response body for this cohort. The current state is read from GET.

Idempotency

POST accepts an optional Idempotency-Key header (max 255 characters). Retrying the same request with the same key returns the original cached response; reusing a key with a different request body returns 409 with SAF-409-001 (Idempotency Conflict). The cache window is 24 hours.
Generate a fresh UUID per logical submission attempt and reuse it across retries until the request resolves successfully.

Cohort mismatch

409 with ONB-409-003 (Onboarding Cohort Mismatch) means the request and the server-derived cohort disagree. It is raised on both methods, for two related reasons: The second case is a 4xx on a read, which is unusual and deliberate. Cohort membership is written once at signup, while the SIS environment features are read live — so an organisation that enables USER_SIGNED_DEPLOY after a user’s cohort was fixed produces a genuine divergence. Reporting it as 409 is the honest answer; the alternative that was previously returned — 200 with step_status: skipped — told the client the platform had waived a step it had in fact never offered. In every case, clients should re-fetch GET /v0/user/me/onboarding/steps to learn the user’s actual cohort flow, then act on it. A POST retry with an unchanged payload will never resolve this.

Rate limit

GET /v0/user/me/onboarding/safe is rate-limited per authenticated user — 60 requests per 60 seconds. Polling clients should respect the limit and back off on 429.

Endpoints

Check Onboarding State

Returns the computed onboarding state. The state is recomputed on every call — reading it records no onboarding events and never advances the current step.
Building a React UI? useOnboardingSteps() from our query patterns handles auth gating via useAuthEnabled() and shares the cached state across components.
Response: 200 OK — see Response Shape above.

Submit Step Completion

Use when the user has completed an action for the current step (e.g. verified their phone, completed KYC, made a selection).
In React, useSubmitOnboardingStep() from our mutation patterns wraps this call and invalidates the cached onboarding query on success — no manual refetch needed.
Request body:
Response: 200 OK for synchronous transitions, 202 Accepted when the submission enqueues background provisioning work (creation flow, KYC completion). Body shape is identical in both cases:
The steps array only contains steps in the user’s cohort flow. A consumer (no org_id) and AGENT_CREATE2 user see five steps. A BYO user sees six (plus byo_safe); a USER_SIGNED_DEPLOY user sees six (plus safe_deploy). Clients should iterate the array rather than indexing into specific positions. As above, the gated values shown are an organisation user’s — see Gating. Behaviour:
  • Records a step_submitted event, then immediately advances to the next step
  • If step is still ahead of the user, returns 409 Conflict with the user’s current step in the response body so the client can resync without a follow-up read
  • Resubmitting a step the user has already finished is an idempotent no-op that returns the current state, so a duplicate submit never needs handling
  • Calling submit on an already-completed user is a safe no-op that returns the terminal state
  • On a 202 Accepted, the response includes a Retry-After header (seconds) recommending the next poll cadence for GET /v0/user/me/onboarding/steps

Get Event History

Returns the full chronological audit trail of onboarding events. Response: 200 OK

Step Reference

Endpoints referenced above:

Integration Patterns

Building an Onboarding UI

  1. After account creation, fetch onboarding state.
  2. Find the step with "status": "current" and render that step’s UI.
  3. When the user completes the step’s action, submit the step.
  4. The response contains the updated state — re-render based on the new current step.
  5. Repeat until is_complete is true.
Endpoints used in this loop: Pseudocode:
Progress bar: Calculate progress from the steps array:

Handling Configurable Steps

Steps with "gated": true may have "status": "skipped" if the platform has disabled them. When building a step list UI:
  • Filter the steps array to exclude skipped steps, or show them as completed
  • Do not hardcode which steps exist — always derive from the steps array
  • The order in the array is the canonical step order

Implicit Step Advancement

Two steps auto-advance without requiring an explicit submit call: Phone verification: When the user successfully verifies their phone, the phone_verification onboarding step is automatically submitted. After calling the phone verification endpoint, re-fetch onboarding state to see the updated step. PUT /v0/user/me/phone/code — Confirm the SMS code. KYC verification: When the KYC status endpoint returns an approved result and the user is currently on the kyc_verification step, it is automatically submitted. After initiating KYC, poll the status endpoint and re-fetch onboarding state once approved. GET /v0/kyc/status — Poll for the current KYC status. In both cases, the client should re-fetch state after the triggering action to see the advancement:

Error Handling

All error responses follow RFC 7807 Problem Details format. The error_code field is the stable machine-readable identifier; detail is human-readable context for the specific occurrence. 401 Unauthorized:
404 Not Found — the authenticated identity has no matching user record yet:
409 Conflict — the submitted step is still ahead of the user. Only that direction conflicts: a step the user has already walked past is answered 200 with the current state, so a duplicate or late submit needs no handling. The detail names both the submitted step and the user’s actual current step so the client can resync without a follow-up GET:
422 Unprocessable Content — request body validation (e.g. missing step field):
502 Bad Gateway — the service that decides which onboarding flow applies to this user is unavailable:
The user’s cohort is written once and then permanent, so when the resolver’s dependency is down the flow is left unresolved rather than guessed. Nothing is persisted and nothing is lost — retry the request. Every endpoint that can start or read onboarding inherits this failure, and each declares it: GET /v0/user/me is the deliberate exception: a profile read falls back to the user’s cached onboarding step rather than answering 502, because the cached value can only lag, never point the wrong way.

Polling After 202 Accepted

When the API returns 202 Accepted, background provisioning is in flight and the response includes a Retry-After header that is the recommended seconds to wait before the next poll of GET /v0/user/me/onboarding/steps. Recommended client behaviour:
  • Honour Retry-After if present.
  • If you implement your own backoff, start at 2 seconds and back off exponentially (e.g. 2s → 4s → 8s) up to a 30-second ceiling, until current_step advances or is_complete becomes true.
  • Do not poll faster than once per second.
  • A client that keeps seeing the same current_step after several polls should surface a “still working” UI rather than retrying indefinitely.

Reference Tables

Step Identifiers

Event Types

Status Values