Skip to main content

TL;DR

New customers are non-custodial by default. A fresh customer first enables crypto via POST /v2/customers/:id/features { type: crypto_wallet }, then claims non-custodial control of their wallet account through POST /v2/customers/:id/wallets/claim-non-custodial before any wallet address is issued. Calling POST /v2/customers/:id/wallets first returns 422 WALLET_NO_PROVIDER_ACCOUNT pointing back at the claim. Calling claim-non-custodial before the CRYPTO_WALLET feature is approved returns 422 CRYPTO_FEATURE_NOT_APPROVED. For orgs Conduit has configured for custodial wallets, that first POST /v2/customers/:id/wallets instead provisions a custodial account directly — no claim — and payouts skip the cosign gate. The two custody models differ in four observable ways: whether the payout parks at a cosign gate, when chain finality fires, how concurrent payouts are handled, and the Travel Rule pre-broadcast window.

What changes between the two models

Cosign gate

  • Custodial: payouts auto-broadcast after compliance checks. No customer-side signature step.
  • Non-custodial: payouts park at pending_cosign and require POST /v2/sandbox/payouts/:id/simulate/cosign {outcome: "approved" | "declined"} to drive forward.
The cosign column on this page describes the passkey-required signing mode (the sandbox default). A wallet in a programmatic signing mode parks the same way, but resolution is a machine stamp rather than a passkey: submit a real P-256 stamp via POST /v2/signing-requests/{id}/approve (real stamps verify in sandbox unmodified), or use the per-signer shortcut POST /v2/sandbox/payouts/:id/simulate-stamp. See Programmatic payout signing and the multi-signer sandbox recipe.

Chain finality autopilot

  • Custodial: the integrator drives chain confirmation explicitly with POST /v2/sandbox/payouts/:id/simulate/confirm {outcome: "completed", txHash}. Production: real chain confirmation.
  • Non-custodial: after cosign approves, the chain-confirm autopilot fires within ~5s and drives the payout to completed. No simulate/confirm call needed.

Concurrent payouts (non-custodial only)

A non-custodial wallet+chain runs one signing ceremony at a time — and that seat is shared across payouts and conversion/offramp source-leg approvals, so a conversion signing from the same wallet+chain queues behind an in-flight payout (and vice versa). Additional payouts are queued. queuePosition is not in the POST /v2/payouts response — it is populated as the payout enters the queue gate (just after the 202) and is read via GET /v2/payouts/:id. The active/head payout omits the field. The queue is bounded: once full, POST /v2/payouts returns 422 PAYOUT_QUEUE_FULL. Recovery: poll GET /v2/payouts/:id for queuePosition, or wait for the head payout to terminalize. See PAYOUT_QUEUE_FULL reference.

Pre-broadcast Travel Rule reject

A counterparty rejection arrives pre-broadcast as long as the payout has not yet broadcast on-chain. On the non-custodial flow Travel Rule runs before signing, so a counterparty reject is pre-broadcast — and terminates the payout with TRAVEL_RULE_REJECTED — throughout the signing window, right up to broadcast. It only becomes audit-only once the payout has broadcast. Two reliable paths to a pre-broadcast TR-reject:
  • Non-custodial wallet, manual counterparty call: simulate/counterparty-webhook {outcome: "rejected", reason} once the Travel Rule row exists — it is created at the pre-signing Travel Rule step, so wait for transaction.processing (which fires right after) before calling; a call made earlier, during compliance screening, returns 404 (no row yet). From transaction.processing through broadcast the reject terminates the payout with TRAVEL_RULE_REJECTED, whether the signers have stamped yet or not.
  • Unconditional pre-broadcast suffix: destination ending bad7e517 (TR_TX_VALIDATE_REJECTED). No manual call needed, and no quorum timing to track.
See Travel Rule scenarios.

How to provision a non-custodial wallet

Once the customer is KYB-approved AND has an approved CRYPTO_WALLET feature on file (submit it via POST /v2/customers/:id/features { type: crypto_wallet } — sandbox-default auto-approve; live-default manual review), call POST /v2/customers/:id/wallets/claim-non-custodial with a roster (admins + signers) and a signingThreshold. The claim endpoint provisions the underlying wallet account, mints the multi-signer roster, and writes the wallets — on every supported chain by default, or just the ones named in an optional chains array. The three EVM chains share one address and one admin approval. After every roster member enrolls, POST /v2/customers/:id/wallets can add a wallet on any chain you excluded. The valid roster shapes depend on the customer’s configured signing mode — passkey-only by default, machine (api_key) signers in the programmatic modes; see Add a crypto wallet. See Sandbox quickstart Step 5 for the full walkthrough.

Lifecycle, side by side

Recipe pairs

Common pitfalls

  • Queue full on non-custodial: PAYOUT_QUEUE_FULL (422). The wallet+chain signing queue is at capacity — and it counts conversion/offramp source-leg approvals from the same wallet+chain, not just payouts. Wait for the head signing ceremony to terminalize, then retry. See PAYOUT_QUEUE_FULL reference.
  • Mixed-case EVM addresses: any mixed-case EVM destination address that is not a valid EIP-55 checksum returns 400 INVALID_ADDRESS_FORMAT. Use all-lowercase. See INVALID_ADDRESS_FORMAT.
  • Pre/post-broadcast TR-reject timing: the cutoff is the on-chain broadcast, not cosign approval. Non-custodial Travel Rule runs before signing, so a simulate/counterparty-webhook reject is pre-broadcast (terminates TRAVEL_RULE_REJECTED) at any point in the signing window — before or after simulate/cosign is approved. It becomes audit-only on the TR row only once the payout has broadcast.
  • txHash constraint on simulate/confirm: the txHash you pass to simulate/confirm does not need to be a real on-chain hash (sandbox is isolated from real chains), but it must be a hex-shaped 32-byte string (0x + 64 hex chars). Any arbitrary 0xdeadbeef... of the right length works.

Error handling on the claim endpoint

POST /v2/customers/:customerId/wallets/claim-non-custodial is the canonical entry point into the multi-signer non-custodial flow. Three 409s plus two 422s are worth handling explicitly:
  • WALLET_NO_PROVIDER_ACCOUNT (422) — returned from POST /v2/customers/:id/wallets when the customer has no wallet account yet — the common fresh-customer case, before any claim. Claim non-custodial control first with POST /v2/customers/{id}/wallets/claim-non-custodial, then retry creating the wallet.
  • WALLET_CUSTODY_NOT_CLAIMED (409) — returned from POST /v2/customers/:id/wallets only when the customer already has a custodial wallet account and the custodial path is not enabled for your organization. Fresh non-custodial customers never hit this; where the custodial path is enabled, the same call returns the wallet directly.
  • CUSTOMER_ALREADY_NON_CUSTODIAL (409) — non-custodial control has already been claimed for this customer. The claim endpoint provisions only fresh customers. Use the roster-management endpoints (POST/DELETE /v2/customers/:customerId/wallet-signers, POST .../promote, POST .../demote) to modify signers instead.
  • CUSTOMER_ALREADY_CUSTODIAL (409) — the customer is a legacy custodial customer (provisioned via the CRYPTO_WALLET application before the non-custodial gate). Multi-signer roster claim on existing custodial wallets will land in a follow-up; until then these customers stay on the custodial path.
  • CRYPTO_FEATURE_NOT_APPROVED (422) — the customer has no active CRYPTO_WALLET feature row, or the feature application is still pending review. Submit POST /v2/customers/:id/features { type: crypto_wallet } and wait for status: approved before retrying. In sandbox the default is auto-approve; in live the default is manual review unless the org has opted out via the per-org cryptoEnablementRequiresReview setting.

See also