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

# Create an identity Sigil for a user

> Take a partner user from zero to a KYC-verified account with a deployed Safe wallet and a real balance call.

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

At the end of this page, you will have a user whose `safe_creation_status` is `completed`, whose `primary_smart_wallet_address` is populated, and a wallet assets call that returns a JSON body listing their balances. This is the full partner onboarding flow — account creation, <Tooltip headline="KYC" tip="Know Your Customer — identity verification that feeds attestation claims onto credentials." cta="Glossary →" href="/glossary">KYC</Tooltip>, Safe deployment, and first read. **When the Safe lands, the identity is live. Everything downstream is a read against what this flow wrote.**

The final read:

* [`GET /v0/wallets/{wallet_id}/assets`](/api-reference/wallet-assets/list-wallet-assets) — List wallet assets.

## Prerequisites

* A Platform API JWT from your [configured auth provider](/dashboard/auth-providers) — Dynamic Labs, Privy, or <Tooltip headline="SIWE" tip="Sign-In With Ethereum — wallet-based authentication." cta="Glossary →" href="/glossary">SIWE</Tooltip>. See [Authentication](/authentication).
* Your Sumvin organisation ID for the `x-juno-orgid` header — every partner integration scopes requests to an org.
* SumSub sandbox credentials — only required for the full WebSDK KYC flow. Skip if you plan to use [document-only mode](/guides/kyc#document-only-mode).
* Base URL `https://api.sumvin.com/v0`.
* Default chain for this quickstart: `chain_id: 1329` (Sei).

<Note>
  SumSub sandbox credentials are only needed when you integrate SumSub's hosted WebSDK or run the hybrid KYC flow. If your integration uses [document-only mode](/guides/kyc#document-only-mode), you upload documents through the Sumvin API and do not need SumSub credentials yourself — contact your account manager only if WebSDK or hybrid is in scope.
</Note>

<Steps>
  <Step title="Create the user">
    Create the user record with their primary EOA address. The response includes a `safe_creation_status: processing` — the Safe deployment worker has started in the background while you work through the remaining steps.

    <Warning>
      The EOA must already be verified by your auth provider (Dynamic Labs or Privy) before this call. Sumvin rejects unverified addresses — the JWT in `x-juno-jwt` is how we confirm the user owns the EOA. If the provider has not completed its wallet verification step, the account creation call returns `403 Forbidden`.

      [`POST /v0/user/`](/api-reference/user/create-user-account) — Create the user account.
    </Warning>

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

      ```typescript TypeScript theme={null}
      const res = await fetch("https://api.sumvin.com/v0/user/", {
        method: "POST",
        headers: {
          "x-juno-jwt": process.env.SUMVIN_JWT!,
          "x-juno-orgid": process.env.SUMVIN_ORG_ID!,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          primary_eoa_address: "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
          chain_id: 1329,
        }),
      });
      const { user } = await res.json();
      ```

      ```python Python theme={null}
      import httpx
      import os

      res = httpx.post(
          "https://api.sumvin.com/v0/user/",
          headers={
              "x-juno-jwt": os.environ["SUMVIN_JWT"],
              "x-juno-orgid": os.environ["SUMVIN_ORG_ID"],
          },
          json={
              "primary_eoa_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
              "chain_id": 1329,
          },
      )
      user = res.json()["user"]
      ```

      ```tsx React theme={null}
      import { useCreateUser } from "@sumvin/api-hooks";

      function CreateUserButton() {
        const { mutate, isPending } = useCreateUser();

        const handleClick = () => {
          mutate({
            primary_eoa_address: "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
            chain_id: 1329,
          });
        };

        return (
          <button onClick={handleClick} disabled={isPending}>
            {isPending ? "Creating…" : "Create user"}
          </button>
        );
      }
      ```
    </CodeGroup>

    <Tip>
      Using TanStack Query? See [mutation patterns](/guides/tanstack-query-patterns#mutation-patterns) for the retry + `isNonRetryable` filter `useCreateUser` applies on failure.
    </Tip>

    **Response:** `201 Created` with `user.safe_creation_status: "processing"`.
  </Step>

  <Step title="Walk the onboarding state machine">
    The onboarding state machine is server-driven — you fetch the current step, submit the prompted data, and poll again. Step statuses are `pending`, `current`, `submitted`, `completed`, or `skipped`.

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

    **Response:** `200 OK`

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

    For `phone_verification`, request a code and then confirm it. `kyc_verification` advances automatically as soon as the SumSub review completes. See the [onboarding guide](/guides/onboarding) for every step in detail.

    * [`PUT /v0/user/me/phone`](/api-reference/user/update-phone) — Request a verification code.
    * [`PUT /v0/user/me/phone/code`](/api-reference/user/verify-phone) — Confirm the code.
  </Step>

  <Step title="Complete KYC">
    Sumvin supports three KYC modes:

    * **`websdk`** — launch SumSub's hosted web SDK on the client. Simplest flow.
    * **`hybrid`** — mix of Sumvin-collected fields plus SumSub SDK.
    * **`document_only`** — upload document images directly through the KYC documents endpoint.

    The quickstart path is `websdk`:

    <Info>
      To use the WebSDK path, your organisation needs its own SumSub access token provisioned on the Sumvin side. Contact Sumvin directly to have it set up before you reach this step — without it, the access-token exchange returns `500 Internal Server Error` with `KYC-500-002`. Document-only mode does not require this.

      [`POST /v0/kyc/access-token`](/api-reference/kyc/get-access-token) — Exchange for a SumSub access token.
    </Info>

    ```bash theme={null}
    # 1. Exchange for a SumSub access token.
    curl -X POST https://api.sumvin.com/v0/kyc/access-token \
      -H "x-juno-jwt: <your-jwt-token>" \
      -H "x-juno-orgid: <your-org-id>"
    ```

    Response:

    ```json theme={null}
    { "access_token": "_act-sbx-...", "expires_at": 1740000900000 }
    ```

    ```bash theme={null}
    # 2. Launch the SumSub Web SDK on the client with that token.
    #    (SumSub's own JS SDK — not a Sumvin API call.)

    # 3. Poll for KYC status once the user finishes the SDK flow.
    curl "https://api.sumvin.com/v0/kyc/status?refresh=true" \
      -H "x-juno-jwt: <your-jwt-token>" \
      -H "x-juno-orgid: <your-org-id>"
    ```

    Response when approved:

    ```json theme={null}
    {
      "status": "approved",
      "verified_at": 1740001200000,
      "_links": {
        "self": { "href": "/v0/kyc/status" },
        "user": { "href": "/v0/user/me" }
      }
    }
    ```

    See the [KYC guide](/guides/kyc) for the `hybrid` and `document_only` branches.
  </Step>

  <Step title="Add the smart account wallet">
    Adding a wallet and marking it primary triggers Safe deployment on the wallet's chain if a Safe does not already exist there. Create the wallet record, then `PATCH` it to promote it.

    <Warning>
      The wallet's EOA address must be verified by your auth provider (Dynamic Labs or Privy) before this call — `credential_id` references a verified credential carried in your authentication JWT. If the supplied `credential_id` is not present in your JWT's verified credentials for the calling user, the endpoint returns `403 Forbidden` with `WAL-403-003` (`WALLET_NOT_VERIFIED`). For the adjacent `POST /v0/user/` flow, passing a `primary_eoa_address` that isn't in your verified credentials returns the same `403 Forbidden` with `WAL-403-003`.
    </Warning>

    <CodeGroup>
      ```bash curl theme={null}
      # Create the wallet
      curl -X POST https://api.sumvin.com/v0/wallets/ \
        -H "x-juno-jwt: <your-jwt-token>" \
        -H "x-juno-orgid: <your-org-id>" \
        -H "Content-Type: application/json" \
        -d '{ "credential_id": "cred_abc123" }'

      # Promote to primary — triggers Safe creation on chain 1329
      curl -X PATCH https://api.sumvin.com/v0/wallets/wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
        -H "x-juno-jwt: <your-jwt-token>" \
        -H "x-juno-orgid: <your-org-id>" \
        -H "Content-Type: application/json" \
        -d '{ "is_primary": true }'
      ```

      ```typescript TypeScript theme={null}
      // Create the wallet
      const createRes = await fetch("https://api.sumvin.com/v0/wallets/", {
        method: "POST",
        headers: {
          "x-juno-jwt": process.env.SUMVIN_JWT!,
          "x-juno-orgid": process.env.SUMVIN_ORG_ID!,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ credential_id: "cred_abc123" }),
      });
      const wallet = await createRes.json();

      // Promote to primary — triggers Safe creation
      const patchRes = await fetch(
        `https://api.sumvin.com/v0/wallets/${wallet.id}`,
        {
          method: "PATCH",
          headers: {
            "x-juno-jwt": process.env.SUMVIN_JWT!,
            "x-juno-orgid": process.env.SUMVIN_ORG_ID!,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ is_primary: true }),
        },
      );
      const patched = await patchRes.json();
      // patchRes.status === 202 when a Safe needs creation
      ```

      ```python Python theme={null}
      import httpx
      import os

      headers = {
          "x-juno-jwt": os.environ["SUMVIN_JWT"],
          "x-juno-orgid": os.environ["SUMVIN_ORG_ID"],
      }

      # Create the wallet
      create = httpx.post(
          "https://api.sumvin.com/v0/wallets/",
          headers=headers,
          json={"credential_id": "cred_abc123"},
      )
      wallet = create.json()

      # Promote to primary — triggers Safe creation
      patch = httpx.patch(
          f"https://api.sumvin.com/v0/wallets/{wallet['id']}",
          headers=headers,
          json={"is_primary": True},
      )
      # patch.status_code == 202 when a Safe needs creation
      ```

      ```tsx React theme={null}
      import { useCreateWallet, useUpdateWallet } from "@sumvin/api-hooks";

      function AddPrimaryWallet() {
        const createWallet = useCreateWallet();
        const updateWallet = useUpdateWallet();

        const handleAdd = async () => {
          const wallet = await createWallet.mutateAsync({
            credential_id: "cred_abc123",
          });
          // Promote to primary — optimistically flips is_primary in the cache,
          // server may return 202 Accepted while Safe deployment runs.
          updateWallet.mutate({ walletId: wallet.id, is_primary: true });
        };

        return <button onClick={handleAdd}>Add primary wallet</button>;
      }
      ```
    </CodeGroup>

    <Tip>
      Using TanStack Query? See [optimistic mutations](/guides/tanstack-query-patterns#optimistic-mutations) — `useUpdateWallet` flips `is_primary` in the cache during `onMutate` and rolls back on error, so the UI never waits for the 202 round trip.
    </Tip>

    **PATCH response:** `202 Accepted`

    ```json theme={null}
    {
      "id": "wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
      "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
      "chain_id": 1329,
      "is_primary": true,
      "is_eoa": true,
      "nickname": null,
      "logo_uri": null,
      "created_at": 1740000001000,
      "deleted_at": null,
      "safe_creation_event_id": "evt_safe_9a7d2f",
      "_links": {
        "self": { "href": "/v0/wallets/wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" },
        "user": { "href": "/v0/user/me" },
        "safe-status": { "href": "/v0/user/me" }
      }
    }
    ```

    See the [wallets guide](/guides/wallets) for the three ownership verification methods (`credential_id`, SIWE, and raw address).
  </Step>

  <Step title="Poll for Safe deployment">
    Poll the user account endpoint until `safe_creation_status` is `completed` and `primary_smart_wallet_address` is populated.

    [`GET /v0/user/me`](/api-reference/user/get-user-account) — Fetch the user account.

    ```bash theme={null}
    curl https://api.sumvin.com/v0/user/me \
      -H "x-juno-jwt: <your-jwt-token>" \
      -H "x-juno-orgid: <your-org-id>"
    ```

    **Response:** `200 OK`

    ```json theme={null}
    {
      "user": {
        "id": "usr_abc123",
        "primary_eoa_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
        "primary_smart_wallet_address": "0x9f8e7d6c5b4a39281726354091827364501928a7",
        "safe_creation_status": "completed"
      },
      "_links": {
        "self": { "href": "/v0/user/me" },
        "wallets": { "href": "/v0/wallets" },
        "kyc-status": { "href": "/v0/kyc/status" }
      }
    }
    ```

    <Note>
      On Sei testnet, Safe deployment typically completes in under 60 seconds. Poll on a 2–5 second interval and give up after \~3 minutes — any longer indicates an infra issue worth escalating.
    </Note>
  </Step>

  <Step title="Read balances">
    This is the working artefact. Query the new primary wallet's assets and confirm the integration is live.

    <CodeGroup>
      ```bash curl theme={null}
      curl https://api.sumvin.com/v0/wallets/wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/assets \
        -H "x-juno-jwt: <your-jwt-token>" \
        -H "x-juno-orgid: <your-org-id>"
      ```

      ```typescript TypeScript theme={null}
      const res = await fetch("https://api.sumvin.com/v0/wallets/wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/assets", {
        headers: {
          "x-juno-jwt": process.env.SUMVIN_JWT!,
          "x-juno-orgid": process.env.SUMVIN_ORG_ID!,
        },
      });
      console.log(await res.json());
      ```

      ```python Python theme={null}
      import httpx
      import os

      res = httpx.get(
          "https://api.sumvin.com/v0/wallets/wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/assets",
          headers={
              "x-juno-jwt": os.environ["SUMVIN_JWT"],
              "x-juno-orgid": os.environ["SUMVIN_ORG_ID"],
          },
      )
      print(res.json())
      ```

      ```tsx React theme={null}
      import { useWalletAssets } from "@sumvin/api-hooks";

      function WalletBalances() {
        const { data, isLoading, error } = useWalletAssets(
          "wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
        );

        if (isLoading) return <p>Loading balances…</p>;
        if (error) return <p>Failed to load balances.</p>;

        return (
          <ul>
            {data?.assets.map((asset) => (
              <li key={asset.symbol}>
                {asset.symbol}: {asset.balance} (${asset.balance_usd})
              </li>
            ))}
          </ul>
        );
      }
      ```
    </CodeGroup>

    <Tip>
      Using TanStack Query? See [stale-while-revalidate](/guides/tanstack-query-patterns#stale-while-revalidate) — `useWalletAssets` caches for 5 minutes and refetches in the background, so balance reads stay snappy without hammering the endpoint.
    </Tip>

    **Response:** `200 OK`

    ```json theme={null}
    {
      "wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
      "wallet_chain_id": 1329,
      "assets": [
        {
          "symbol": "ETH",
          "name": "Ether",
          "chain_id": 1329,
          "balance": "0.05423100",
          "balance_usd": "173.54",
          "balance_updated_at": 1740000010000,
          "transactions_href": "/v0/wallets/wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/assets/eth-1329/transactions",
          "_links": {
            "self": { "href": "/v0/wallets/wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/assets" }
          }
        }
      ],
      "total_assets": 1,
      "_links": {
        "self": { "href": "/v0/wallets/wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/assets" },
        "wallet": { "href": "/v0/wallets/wal-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" }
      }
    }
    ```

    <Check>Your partner user has a live, KYC-verified Sumvin account with a deployed Safe and a real balance. You are ready to enrol them in agentic flows.</Check>
  </Step>
</Steps>

## What's next

<ol className="counter-style">
  <li>**[Set up an Errand](/guides/quickstarts/set-up-errand)** — create an IPA and watch it transition through the agentic commerce lifecycle.</li>
  <li>**[Onboarding guide](/guides/onboarding)** — full reference for every onboarding step and status.</li>
  <li>**[KYC guide](/guides/kyc)** — `websdk`, `hybrid`, and `document_only` KYC modes in depth.</li>
  <li>**[Wallets guide](/guides/wallets)** — multi-chain wallet management and Safe deployment details.</li>
</ol>
