> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sumvin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Onboarding

# User Onboarding

<Snippet file="product-term-disambiguation.mdx" />

## 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

```bash theme={null}
curl -X POST /v0/user/ \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>" \
  -H "Content-Type: application/json" \
  -d '{
    "primary_eoa_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
    "chain_id": 1329
  }'
```

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

### 2. Check onboarding state

```bash theme={null}
curl /v0/user/me/onboarding/steps \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"
```

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:

```bash theme={null}
curl -X POST /v0/user/me/onboarding/steps \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>" \
  -H "Content-Type: application/json" \
  -d '{"step": "phone_verification"}'
```

### 4. Onboarding complete

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

```bash theme={null}
curl /v0/user/me/onboarding/steps \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"
# → { "onboarding": { "current_step": "complete", "is_complete": true, ... } }
```

## Onboarding State

### Reading State

Fetch the current onboarding state from the dedicated resource:

```
GET /v0/user/me/onboarding/steps
```

### Response Shape

```json theme={null}
{
  "onboarding": {
    "current_step": "kyc_verification",
    "is_complete": false,
    "steps": [
      { "step": "phone_verification", "status": "completed", "gated": false, "meta": null },
      { "step": "kyc_verification",   "status": "current",   "gated": false, "meta": { "kyc_mode": "websdk" } },
      { "step": "open_banking",       "status": "pending",   "gated": true,  "meta": null },
      { "step": "card_setup",         "status": "pending",   "gated": true,  "meta": null },
      { "step": "feature_selection",  "status": "pending",   "gated": true,  "meta": null }
    ]
  },
  "_links": { ... }
}
```

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](#gating).

| Field            | Type           | Description                                                                              |
| ---------------- | -------------- | ---------------------------------------------------------------------------------------- |
| `current_step`   | string         | The step the user is currently on, or `"complete"`                                       |
| `is_complete`    | boolean        | `true` when all steps are finished                                                       |
| `steps`          | array          | All onboarding steps in order                                                            |
| `steps[].step`   | string         | Step identifier                                                                          |
| `steps[].status` | string         | One of: `pending`, `current`, `submitted`, `completed`, `skipped`                        |
| `steps[].gated`  | boolean        | Whether the platform is able to skip this step **for this user** (see [Gating](#gating)) |
| `steps[].meta`   | object or null | Step-specific configuration metadata (see [Step Metadata](#step-metadata))               |

### Step Statuses

| Status      | Meaning                                                                                                     |
| ----------- | ----------------------------------------------------------------------------------------------------------- |
| `pending`   | Step has not been reached yet                                                                               |
| `current`   | User is on this step now — action required                                                                  |
| `submitted` | User submitted this step (transient — auto-advances immediately)                                            |
| `completed` | Step finished successfully                                                                                  |
| `skipped`   | Step is in this user's flow but the platform waived it — no action required, and it never becomes `current` |

### 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:

| User            | What can waive a step                                                          | Steps that report `gated: true`                                                             |
| --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| No `org_id`     | Per-step platform kill switches                                                | `phone_verification`, `kyc_verification`, `open_banking`, `card_setup`, `feature_selection` |
| Has an `org_id` | The [environment features](/dashboard/features) enabled for their organisation | `open_banking`, `card_setup`, `feature_selection`                                           |

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

| Step                 | `meta` keys | Notes                                                                                                                                                                            |
| -------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `phone_verification` | —           | Always `null`.                                                                                                                                                                   |
| `kyc_verification`   | `kyc_mode`  | Always populated; see below.                                                                                                                                                     |
| `byo_safe`           | `chain_id`  | Only present in the BYO cohort flow; `chain_id` populated when the user has a `primary_chain_id` (see [Wallet cohort](#wallet-cohort-byo_safe-vs-safe_deploy)).                  |
| `safe_deploy`        | `chain_id`  | Only present in the USER\_SIGNED\_DEPLOY cohort flow; `chain_id` populated when the user has a `primary_chain_id` (see [Wallet cohort](#wallet-cohort-byo_safe-vs-safe_deploy)). |
| `open_banking`       | —           | Always `null`.                                                                                                                                                                   |
| `card_setup`         | —           | Always `null`.                                                                                                                                                                   |
| `feature_selection`  | —           | Always `null`.                                                                                                                                                                   |

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.

| Key        | Values                                    | Description                                                                                                                                                                                                |
| ---------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kyc_mode` | `"websdk"`, `"hybrid"`, `"document_only"` | Which <Tooltip headline="KYC" tip="Know Your Customer — identity verification that feeds attestation claims onto credentials." cta="Glossary →" href="/glossary">KYC</Tooltip> verification flow to render |

* `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](/guides/kyc#kyc-modes) for integration details, including how partner organisations can override the mode per environment.

### State Machine

```mermaid theme={null}
stateDiagram-v2
    [*] --> created : POST /v0/user/
    created --> phone_verification : start onboarding
    created --> kyc_verification : (phone skipped — no org_id only)
    created --> complete : (all steps skipped)

    phone_verification --> kyc_verification : submit / auto-advance
    kyc_verification --> byo_safe : (BYO_SAFE cohort)
    kyc_verification --> safe_deploy : (USER_SIGNED_DEPLOY cohort)
    kyc_verification --> open_banking : (agent or non-org cohort — neither wallet step is in the flow)
    byo_safe --> open_banking : submit
    safe_deploy --> open_banking : submit / auto-advance
    open_banking --> card_setup : submit
    card_setup --> feature_selection : submit
    feature_selection --> complete : submit

    note right of phone_verification : Skippable only without an org_id
    note right of byo_safe : Cohort: BYO_SAFE
    note right of safe_deploy : Cohort: USER_SIGNED_DEPLOY
```

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](#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.

| `AI_AGENT` | `USER_SIGNED_DEPLOY` | `byo_safe` | `safe_deploy` | What runs                                                                                                        |
| ---------- | -------------------- | ---------- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
| enabled    | disabled             | absent     | absent        | Sumvin's signing service deploys a Safe with an agent signer attached. No user action.                           |
| disabled   | enabled              | absent     | in flow       | The user signs the Safe-deployment UserOp themselves (see [Safe user operations](/guides/safe-user-operations)). |
| disabled   | disabled             | in flow    | absent        | The user submits the address of a Safe they already control on-chain.                                            |
| enabled    | enabled              | —          | —             | Rejected at SIS feature-write — `SIS-409-008`. See [Environment features](/dashboard/features).                  |

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  /v0/user/me/onboarding/safe
POST /v0/user/me/onboarding/safe
```

`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](#cohort-mismatch) below).

#### `mode=byo`

```bash theme={null}
curl -X POST https://api.sumvin.com/v0/user/me/onboarding/safe \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>" \
  -H "Content-Type: application/json" \
  -d '{"address": "0x..."}'
```

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:

| The user is                         | Status | Error code                                            | Recovery                                                                                                                    |
| ----------------------------------- | ------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Still on an earlier step            | `400`  | **`ONB-400-002`** (BYO Safe Step Required)            | Sequencing, not conflict — the step is theirs and still ahead. Complete the prior steps; the same submission then succeeds. |
| Already past this step              | `409`  | **`ONB-409-002`** (Onboarding Step Already Completed) | The step is closed. Read `GET` for the current state.                                                                       |
| In a cohort with no `byo_safe` step | `409`  | **`ONB-409-003`** (Onboarding Cohort Mismatch)        | Re-fetch `GET` to learn the cohort — see [Cohort mismatch](#cohort-mismatch).                                               |

#### `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`:

```json theme={null}
{
  "mode": "user_signed_deploy",
  "config": {
    "mode": "user_signed_deploy",
    "chain_id": 1329,
    "user_operation": null,
    "user_op_hash": null,
    "predicted_safe_address": null,
    "user_eoa": null,
    "salt_nonce": null
  },
  "wallet": null,
  "_links": { ... }
}
```

Once prepared, the user signs the hash with their EOA and replays it to `POST`:

```bash theme={null}
curl -X POST https://api.sumvin.com/v0/user/me/onboarding/safe \
  -H "x-juno-jwt: <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "user_operation": { "...": "...", "signature": "0x<user-eoa-signature>" },
    "expected_user_op_hash": "0x..."
  }'
```

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](/guides/safe-user-operations) 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:

```bash theme={null}
curl -X POST https://api.sumvin.com/v0/user/me/onboarding/safe \
  -H "x-juno-jwt: <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
# → HTTP 204 No Content (empty body)
```

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.

```bash theme={null}
curl -X POST https://api.sumvin.com/v0/user/me/onboarding/safe \
  -H "x-juno-jwt: <token>" \
  -H "Idempotency-Key: 4f8e3b21-9d2c-4a7e-bb5f-1c8a09e2d3f1" \
  -H "Content-Type: application/json" \
  -d '{"address": "0x..."}'
```

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:

| Method           | Raised when                                                                                                                                                    | Typical cause                                                                                             |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `POST`           | The submitted payload *shape* does not match the resolved cohort — e.g. a BYO-shaped `{"address": "0x..."}` for a user the server resolved as `agent_create2`. | A stale client cache after an SIS feature flip.                                                           |
| `GET` and `POST` | The user's cohort contains **no** Safe step of the kind being read or submitted.                                                                               | The user's cohort was latched at signup and their organisation's environment features changed afterwards. |

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

```
GET /v0/user/me/onboarding/steps
```

Returns the computed onboarding state. The state is recomputed on every call — reading it records no onboarding events and never advances the current step.

<Tip>
  Building a React UI? `useOnboardingSteps()` from our [query patterns](/guides/tanstack-query-patterns#query-patterns) handles auth gating via `useAuthEnabled()` and shares the cached state across components.
</Tip>

**Response:** 200 OK — see [Response Shape](#response-shape) above.

### Submit Step Completion

```
POST /v0/user/me/onboarding/steps
```

Use when the user has completed an action for the current step (e.g. verified their phone, completed KYC, made a selection).

<Tip>
  In React, `useSubmitOnboardingStep()` from our [mutation patterns](/guides/tanstack-query-patterns#mutation-patterns) wraps this call and invalidates the cached onboarding query on success — no manual refetch needed.
</Tip>

**Request body:**

```json theme={null}
{
  "step": "phone_verification"
}
```

**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:

```json theme={null}
{
  "onboarding": {
    "current_step": "kyc_verification",
    "is_complete": false,
    "steps": [
      { "step": "phone_verification", "status": "completed", "gated": false, "meta": null },
      { "step": "kyc_verification",   "status": "current",   "gated": false, "meta": { "kyc_mode": "websdk" } },
      { "step": "open_banking",       "status": "pending",   "gated": true,  "meta": null },
      { "step": "card_setup",         "status": "pending",   "gated": true,  "meta": null },
      { "step": "feature_selection",  "status": "pending",   "gated": true,  "meta": null }
    ]
  },
  "_links": { ... }
}
```

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](#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

```
GET /v0/user/me/onboarding/events
```

Returns the full chronological audit trail of onboarding events.

**Response:** 200 OK

```json theme={null}
{
  "events": [
    {
      "step": "phone_verification",
      "event_type": "step_entered",
      "from_step": "created",
      "duration_ms": null,
      "created_at": 1700000000000
    },
    {
      "step": "phone_verification",
      "event_type": "step_submitted",
      "from_step": null,
      "duration_ms": null,
      "created_at": 1700000060000
    },
    {
      "step": "phone_verification",
      "event_type": "step_completed",
      "from_step": null,
      "duration_ms": 60000,
      "created_at": 1700000060000
    },
    {
      "step": "kyc_verification",
      "event_type": "step_entered",
      "from_step": "phone_verification",
      "duration_ms": null,
      "created_at": 1700000060000
    }
  ],
  "_links": { ... }
}
```

| Field         | Type            | Description                                                                |
| ------------- | --------------- | -------------------------------------------------------------------------- |
| `step`        | string          | The step this event relates to                                             |
| `event_type`  | string          | One of: `step_entered`, `step_submitted`, `step_completed`, `step_skipped` |
| `from_step`   | string or null  | The previous step at the time of transition                                |
| `duration_ms` | integer or null | Time spent in the step (only on `step_completed` events)                   |
| `created_at`  | integer         | Event timestamp (epoch milliseconds)                                       |

## Step Reference

| Step                 | Description                                                         | Related Endpoints                                                                                                                                                          | Auto-advances?                                            |
| -------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `phone_verification` | Verify phone number via SMS code                                    | Update phone, then verify code — see below                                                                                                                                 | Yes — successful code verification auto-submits this step |
| `kyc_verification`   | Complete identity verification (mode determined by `meta.kyc_mode`) | Start KYC, then poll status — see below                                                                                                                                    | Yes — approved KYC status auto-submits this step          |
| `byo_safe`           | Submit a Safe wallet address you already control                    | [`POST /v0/user/me/onboarding/safe`](/api-reference/user/submit-onboarding-safe) — appears only when the user's organisation is configured for Bring-Your-Own-Safe         | No                                                        |
| `safe_deploy`        | Sign a deploy UserOp to deploy a new Safe                           | [`POST /v0/user/me/onboarding/safe`](/api-reference/user/submit-onboarding-safe) — appears only when the user's organisation is configured for user-signed Safe deployment | Yes — auto-advances after the bundler confirms the deploy |
| `open_banking`       | Connect bank accounts                                               | Explicit submit only                                                                                                                                                       | No                                                        |
| `card_setup`         | Set up payment card                                                 | Explicit submit only                                                                                                                                                       | No                                                        |
| `feature_selection`  | Choose platform features                                            | Explicit submit only                                                                                                                                                       | No                                                        |

**Endpoints referenced above:**

* [`PUT /v0/user/me/phone`](/api-reference/user/update-phone) — Send a verification code.
* [`PUT /v0/user/me/phone/code`](/api-reference/user/verify-phone) — Confirm the code.
* [`GET /v0/kyc/status`](/api-reference/kyc/get-current-kyc-verification-status) — Check KYC result.

## 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:**

* [`GET /v0/user/me/onboarding/steps`](/api-reference/user/get-onboarding-steps) — Fetch computed onboarding state.
* [`POST /v0/user/me/onboarding/steps`](/api-reference/user/submit-onboarding-step-route) — Submit the completed step.

**Pseudocode:**

```
state = GET /v0/user/me/onboarding/steps

while not state.onboarding.is_complete:
    current = find step where status == "current"
    render_step_ui(current.step)
    await user_action()
    state = POST /v0/user/me/onboarding/steps { step: current.step }

show_onboarding_complete()
```

**Progress bar:** Calculate progress from the steps array:

```
total = len(steps)
done = count(steps where status in ["completed", "skipped"])
progress = done / total
```

### 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

```
active_steps = [s for s in steps if s.status != "skipped"]
```

### 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`](/api-reference/user/verify-phone) — 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`](/api-reference/kyc/get-current-kyc-verification-status) — Poll for the current KYC status.

In both cases, the client should re-fetch state after the triggering action to see the advancement:

```bash theme={null}
# Phone verification auto-advances onboarding
curl -X PUT /v0/user/me/phone/code \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>" \
  -d '{"verification_code": "123456"}'

# Re-fetch to see updated onboarding state
curl /v0/user/me/onboarding/steps \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"
```

### 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.

| Status                    | Error code    | When it fires                                                                                                                          |
| ------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| 401 Unauthorized          | `USR-401-001` | Missing or invalid authentication token                                                                                                |
| 404 Not Found             | `USR-404-001` | User account does not exist — call `POST /v0/user/` first                                                                              |
| 409 Conflict              | `ONB-409-001` | The submitted `step` is still **ahead** of the user. Submitting a step they have already finished is an idempotent `200`, not an error |
| 422 Unprocessable Content | (validation)  | Request body missing required `step` field                                                                                             |
| 502 Bad Gateway           | `SAF-502-006` | The service that resolves which onboarding flow applies to this user is unavailable. Retryable — see below                             |

**401 Unauthorized:**

```json theme={null}
{
  "type": "https://api.sumvin.com/errors/usr-401-001",
  "title": "User Authentication Failed",
  "status": 401,
  "detail": "Missing or invalid authentication token",
  "instance": "/v0/user/me/onboarding/steps",
  "error_code": "USR-401-001"
}
```

**404 Not Found** — the authenticated identity has no matching user record yet:

```json theme={null}
{
  "type": "https://api.sumvin.com/errors/usr-404-001",
  "title": "User Not Found",
  "status": 404,
  "detail": "User not found",
  "instance": "/v0/user/me/onboarding/steps",
  "error_code": "USR-404-001"
}
```

**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`:

```json theme={null}
{
  "type": "https://api.sumvin.com/errors/onb-409-001",
  "title": "Wrong Onboarding Step",
  "status": 409,
  "detail": "Submitted step 'feature_selection' does not match the user's current step 'kyc_verification'. Read GET /v0/user/me/onboarding/steps and submit the step with status='current'.",
  "instance": "/v0/user/me/onboarding/steps",
  "error_code": "ONB-409-001"
}
```

**422 Unprocessable Content** — request body validation (e.g. missing `step` field):

```json theme={null}
{
  "detail": [
    {
      "loc": ["body", "step"],
      "msg": "Field required",
      "type": "missing"
    }
  ]
}
```

**502 Bad Gateway** — the service that decides which onboarding flow applies to this user is unavailable:

```json theme={null}
{
  "type": "https://api.sumvin.com/errors/saf-502-006",
  "title": "Safe Verification Service Error",
  "status": 502,
  "detail": "The onboarding flow for this user could not be resolved; retry shortly.",
  "instance": "/v0/user/me/onboarding/steps",
  "error_code": "SAF-502-006"
}
```

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:

| Endpoint                            | 502 error code | Fires when                                                                   |
| ----------------------------------- | -------------- | ---------------------------------------------------------------------------- |
| `POST /v0/user/`                    | `SAF-502-006`  | Account creation starts onboarding, so it resolves the cohort too            |
| `GET /v0/user/me/onboarding/steps`  | `SAF-502-006`  | The cohort resolver's dependency is unavailable                              |
| `POST /v0/user/me/onboarding/steps` | `SAF-502-006`  | As above                                                                     |
| `GET /v0/user/me/onboarding/safe`   | `SAF-502-006`  | On-chain Safe verification is unavailable                                    |
| `POST /v0/user/me/onboarding/safe`  | `SAF-502-006`  | As above                                                                     |
| `POST /v0/user/me/onboarding/safe`  | `SAF-502-007`  | `user_signed_deploy` only — the gas sponsor rejected or could not be reached |

`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

| Value                | Description                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| `created`            | Initial state after account creation (not a user-facing step)                                          |
| `phone_verification` | Phone number verification via SMS                                                                      |
| `kyc_verification`   | Identity verification (KYC)                                                                            |
| `byo_safe`           | Bring-Your-Own-Safe submission — appears only when the user's organisation is configured for that mode |
| `safe_deploy`        | User-signed Safe deployment — appears only when the user's organisation is configured for that mode    |
| `open_banking`       | Bank account connection                                                                                |
| `card_setup`         | Payment card setup                                                                                     |
| `feature_selection`  | Platform feature selection                                                                             |
| `complete`           | Terminal state — onboarding finished                                                                   |

### Event Types

| Value            | Description                                                      |
| ---------------- | ---------------------------------------------------------------- |
| `step_entered`   | User arrived at a new step                                       |
| `step_submitted` | User submitted a step (recorded by submit endpoint, not advance) |
| `step_completed` | Step was completed and user advanced past it                     |
| `step_skipped`   | Step was skipped by platform configuration                       |

### Status Values

| Value       | Description                                           |
| ----------- | ----------------------------------------------------- |
| `pending`   | Step not yet reached                                  |
| `current`   | User is on this step — action required                |
| `submitted` | Step was submitted (transient — advances immediately) |
| `completed` | Step finished                                         |
| `skipped`   | Step disabled by platform configuration               |
