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

# Add a Crypto Wallet

> Provision a customer's non-custodial crypto wallets end to end: enable the feature, claim non-custodial control with a passkey or machine (API-key) signer roster, and activate.

This guide walks the full setup for a customer's non-custodial crypto wallets, from enabling the feature to a set of `active` wallets ready to receive and send. For the concepts behind each step see [Crypto Wallets](/concepts/crypto-wallets), [Non-Custodial Wallets](/concepts/non-custodial-wallets), and [Multi-signer wallets](/concepts/multi-signer-wallets). To move funds afterward, see [Receive Crypto](/guides/receive-crypto-lifecycle) and the [Non-Custodial Payout Lifecycle](/guides/non-custodial-payout-lifecycle).

The customer must already be onboarded and `active`. You get `customerId` from the `application.approved` webhook — see [Onboard a Customer](/guides/onboard-customer). Calling these endpoints before the customer is active returns [`CUSTOMER_NOT_ONBOARDED`](/errors/customer-not-onboarded).

<Note>
  **Test this flow in sandbox.** Drive it end-to-end with simulated money and deterministic controls — start with the [sandbox quickstart](/sandbox/quickstart), then [custodial vs non-custodial setup](/sandbox/custody) for this flow, the [multi-signer recipes](/sandbox/multi-signer-wallets) for roster and signing scenarios, and the [cheat sheet](/sandbox/cheat-sheet) for every magic value and simulate endpoint.
</Note>

## Flow

0. Know the customer's **signing mode** — it is arranged with Conduit, not set via API, and it dictates the roster shapes Step 2 will accept.
1. Request the `crypto_wallet` feature; wait for `application.approved` (`applicationType: "crypto_wallet"`).
2. Claim non-custodial control with a signer roster and threshold. The roster's shape — passkey members, machine (`api_key`) members, or a mix — is set by the customer's **signing mode** (Step 0).
3. Passkey members enroll via their invitation link (delivered on `wallet_signer.invited`); machine members are active immediately, and an all-machine roster skips this step.
4. Wait for `crypto_wallet.completed` — the wallets are now `active`.

No wallet address exists until the claim completes: the `crypto_wallet` feature only makes the customer *eligible* to claim.

## Step 0 — Know the customer's signing mode

Every customer has a **signing mode** that dictates which roster shapes a claim accepts and how the wallets activate. It is configured by Conduit out of band: an org-wide default with an optional per-customer override (the customer-level setting wins). There is no API to set it — arrange it with your Conduit representative. **Programmatic unattended** additionally requires an approval review by Conduit.

| Mode                        | Valid roster shapes                                                                                                                                                               | What happens at activation                                               |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| **Passkey required**        | Every member is `credentialType: "passkey"`.                                                                                                                                      | Wallets activate only after every member enrolls their passkey (Step 3). |
| **Programmatic**            | Admins are passkeys; `api_key` members are allowed with `role: "signer"` only, and the machine keys alone must reach `signingThreshold` (a passkey-only roster is not claimable). | Passkey members still enroll; machine members are active at claim.       |
| **Programmatic unattended** | `api_key` members may hold `role: "admin"` too — a fully machine roster is valid. The governance floor still applies: at least two admins, threshold ≥ 1 and ≤ roster size.       | An all-machine roster activates without any enrollment step.             |

These rules are enforced at claim time and again on every later signer add. A roster that breaks the mode's shape (an `api_key` member under passkey-required, or an `api_key` admin under programmatic) returns `422` [`SIGNING_MODE_ROSTER_INVALID`](/errors#signing-mode-roster-invalid); a programmatic roster whose machine keys cannot cover the threshold — including zero machine keys — returns `422` [`PROGRAMMATIC_QUORUM_UNREACHABLE`](/errors#programmatic-quorum-unreachable).

## Step 1 — Request the crypto-wallet feature

Discover any extra requirements, then submit the feature application. `Idempotency-Key` is required — without it the call returns `IDEMPOTENCY_KEY_REQUIRED`.

```bash theme={null}
curl 'https://api.conduit.financial/v2/customers/{customerId}/features/requirements?type=crypto_wallet' \
  -H "x-api-key: YOUR_API_KEY"

curl https://api.conduit.financial/v2/customers/{customerId}/features \
  -X POST \
  -H "x-api-key: YOUR_API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "type": "crypto_wallet" }'
```

Track the feature application through `application.approved` / `application.rejected` (`applicationType: "crypto_wallet"`). Customers in restricted jurisdictions are rejected up front with [`CRYPTO_NOT_AVAILABLE_IN_JURISDICTION`](/errors); calling the next step before the feature is approved returns `CRYPTO_FEATURE_NOT_APPROVED`.

## Step 2 — Claim non-custodial control

One call mints the customer's signing setup and provisions the initial wallets. The roster needs at least two admins; `signingThreshold` sets how many stamps each payout needs. `chains` is optional — **omit it** to provision a wallet on every supported chain in one claim (recommended). Pick the roster shape that matches the customer's signing mode (Step 0). A machine (`api_key`) member carries a `publicKey`: a compressed P-256 public key you generate and hold — 33 bytes hex-encoded (66 hex chars, `02`/`03` prefix; optional `0x`). Conduit only ever receives the public half; no field anywhere carries a private key. Missing key → `400 API_KEY_PUBLIC_KEY_REQUIRED`; malformed → `400 API_KEY_PUBLIC_KEY_INVALID`; two members sharing one → `422 API_KEY_PUBLIC_KEY_DUPLICATE`.

<Tabs>
  <Tab title="Passkey roster">
    ```bash theme={null}
    curl https://api.conduit.financial/v2/customers/{customerId}/wallets/claim-non-custodial \
      -X POST \
      -H "x-api-key: YOUR_API_KEY" \
      -H "content-type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
        "signingThreshold": 2,
        "roster": [
          { "email": "ada@example.com",   "name": "Ada",   "role": "admin",  "credentialType": "passkey" },
          { "email": "grace@example.com", "name": "Grace", "role": "admin",  "credentialType": "passkey" },
          { "email": "ken@example.com",   "name": "Ken",   "role": "signer", "credentialType": "passkey" }
        ]
      }'
    ```

    The **passkey-required** shape: every member enrolls a passkey in Step 3.
  </Tab>

  <Tab title="Hybrid — programmatic">
    ```bash theme={null}
    curl https://api.conduit.financial/v2/customers/{customerId}/wallets/claim-non-custodial \
      -X POST \
      -H "x-api-key: YOUR_API_KEY" \
      -H "content-type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
        "signingThreshold": 2,
        "roster": [
          { "email": "ada@example.com",      "name": "Ada",      "role": "admin",  "credentialType": "passkey" },
          { "email": "grace@example.com",    "name": "Grace",    "role": "admin",  "credentialType": "passkey" },
          { "email": "signer-1@example.com", "name": "Signer 1", "role": "signer", "credentialType": "api_key", "publicKey": "02a1b2c3..." },
          { "email": "signer-2@example.com", "name": "Signer 2", "role": "signer", "credentialType": "api_key", "publicKey": "03d4e5f6..." }
        ]
      }'
    ```

    **Programmatic** mode: passkey admins govern, machine keys sign. The machine keys alone must be *able* to cover `signingThreshold` — here two keys cover a threshold of 2. Passkey members can still stamp on the verify page, and their stamps count toward the threshold too.
  </Tab>

  <Tab title="Machine-only — unattended">
    ```bash theme={null}
    curl https://api.conduit.financial/v2/customers/{customerId}/wallets/claim-non-custodial \
      -X POST \
      -H "x-api-key: YOUR_API_KEY" \
      -H "content-type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
        "signingThreshold": 2,
        "roster": [
          { "email": "admin-1@example.com",  "name": "Admin 1",  "role": "admin",  "credentialType": "api_key", "publicKey": "02a1b2c3..." },
          { "email": "admin-2@example.com",  "name": "Admin 2",  "role": "admin",  "credentialType": "api_key", "publicKey": "03d4e5f6..." },
          { "email": "signer-1@example.com", "name": "Signer 1", "role": "signer", "credentialType": "api_key", "publicKey": "02b7c8d9..." }
        ]
      }'
    ```

    **Programmatic unattended** mode only: machine keys may hold `admin`. No enrollment step — the wallets activate without any signer action after the claim.
  </Tab>
</Tabs>

Returns `202 Accepted` with a `claimId`. The `claimId` is a correlation receipt, not a status handle — don't poll it; wait for the webhooks. See [Multi-signer wallets](/concepts/multi-signer-wallets) for roster rules (admin vs signer, thresholds, root quorum) and [Crypto Wallets](/concepts/crypto-wallets#creating-wallets) for the shared EVM address and optional `chains`.

## Step 3 — Enroll the passkey members

This step applies to **passkey members only**. Machine (`api_key`) members seat `active` at claim: no invite webhook, no URL, no enrollment (`passkeyCount` stays 0). An **all-machine roster skips this step entirely** — `claim.completed` is your activation signal, with no signer action in between.

For passkey members: Conduit never contacts signers directly. Each one gets a `wallet_signer.invited` webhook carrying their own `verificationUrl` and `expiresAt` — forward each URL to the intended signer. The signer opens it and registers their passkey on-device. A plain signer enrolls one passkey; an admin enrolls two (a backup device).

```json theme={null}
{
  "type": "wallet_signer.invited",
  "data": {
    "customerId": "cus_...",
    "walletSignerId": "wsg_...",
    "verificationUrl": "https://app.conduit.financial/verify/<token>",
    "expiresAt": "2026-01-15T09:30:00.000Z"
  }
}
```

Each signer emits `wallet_signer.enrolled` on their qualifying stamp. See [Non-Custodial Wallets](/concepts/non-custodial-wallets#verify-page) for the hosted verify page. In sandbox, `POST /v2/sandbox/wallet-signers/:signerId/mark-enrolled` collapses a passkey member's enrollment headlessly (machine members never need it).

## Step 4 — Wait for activation

Once every passkey member has enrolled, the wallets activate. Listen for `claim.completed` — it carries the `claimId` from Step 2 plus the activated `walletIds`, so you can close the loop on the claim without polling. A customer-level `crypto_wallet.completed` also fires, carrying `customerId` only (no wallet IDs). Either is your signal the customer can receive deposits and originate payouts.

```bash theme={null}
curl https://api.conduit.financial/v2/customers/{customerId}/wallets \
  -H "x-api-key: YOUR_API_KEY"
```

Every wallet reads `status: "active"` with an `address`. The three EVM chains (`ethereum`, `base`, `polygon`) share one address; `solana` and `tron` each have their own. `GET /wallets` hides wallets until they activate, so this list is empty between the claim and `crypto_wallet.completed`.

## What's next

* **Receive crypto:** share a wallet's `address` — see [Receive Crypto](/guides/receive-crypto-lifecycle).
* **Send crypto:** every payout collects the roster's signatures — see [Non-Custodial Payout Lifecycle](/guides/non-custodial-payout-lifecycle).
* **Sign payouts from your backend:** on a programmatic wallet the signing loop is server-to-server — discover requests, stamp, submit. See [Programmatic payout signing](/guides/machine-signer-stamping).
* **Add a wallet on another chain, or change the roster or threshold later:** see [Multi-signer wallets](/concepts/multi-signer-wallets#lifecycle-endpoints).
