TL;DR
New customers are non-custodial. Every fresh customer claims non-custodial control of their wallet account throughPOST /v2/customers/:id/wallets/claim-non-custodial before any wallet address is issued; calling POST /v2/customers/:id/wallets first returns 409 WALLET_CUSTODY_NOT_CLAIMED. The custodial path covered below applies only to legacy customers that were provisioned through the CRYPTO_WALLET application before the non-custodial gate. The two custody models differ in four observable ways: whether the payout parks at a cosign gate, when chain finality fires, when the in-flight 409 happens, 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_cosignand requirePOST /v2/sandbox/payouts/:id/simulate/cosign {outcome: "approved" | "declined"}to drive forward.
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. Nosimulate/confirmcall needed.
Concurrent cosign in-flight (non-custodial only)
POST /v2/payouts returns 409 CONCURRENT_COSIGN_IN_FLIGHT when the source wallet already has a non-custodial payout awaiting signature on the same chain. Recovery: resolve the prior payout with simulate/cosign first or wait for it to terminalize. Retry with exponential backoff starting at 2 seconds. See CONCURRENT_COSIGN_IN_FLIGHT playbook.
Pre-broadcast Travel Rule reject
A counterparty rejection arrives pre-broadcast only when the cosign gate is still open. Two reliable paths to a pre-broadcast TR-reject:- Non-custodial wallet, manual counterparty call: cosign-gate-parked payout +
simulate/counterparty-webhook {outcome: "rejected", reason}BEFOREsimulate/cosign. - Unconditional pre-broadcast suffix: destination ending
bad7e517(TR_TX_VALIDATE_REJECTED). No manual call needed.
How to provision a non-custodial wallet
Once the customer is KYB-approved, callPOST /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 for the chains you ask for. After every roster member enrolls, POST /v2/customers/:id/wallets becomes usable for each chain you requested. See Sandbox quickstart Step 5 for the full walkthrough.
Lifecycle, side by side
| State / event | Custodial | Non-custodial |
|---|---|---|
Create payout (POST /v2/payouts) | 202 { id: txn_..., status: pending } | 202 { id: txn_..., status: pending_cosign } |
| In-flight conflict (409) | n/a | CONCURRENT_COSIGN_IN_FLIGHT if a prior NC payout on the same wallet awaits signature |
| Cosign | Auto-resolves; no customer call | POST .../simulate/cosign {outcome: "approved" | "declined"} |
| Chain broadcast | Internal; no customer call | After cosign approved |
| Chain finality | Customer calls POST .../simulate/confirm {outcome: "completed", txHash: "..."} | Auto-resolves in ~5s via the chain-confirm autopilot |
| Travel Rule reject (counterparty) | Post-broadcast audit-only; payout already terminal | Pre-broadcast if simulate/counterparty-webhook called before simulate/cosign |
| Force-fail at broadcast | POST .../simulate/broadcast-fail | POST .../simulate/broadcast-fail (same) |
| Reachable failure codes | RAIL_POLICY_REJECTED, RAIL_UNAVAILABLE, INSUFFICIENT_FUNDS_AT_SETTLE | All custodial codes + USER_SIGNATURE_DECLINED, USER_SIGNATURE_TIMEOUT, USER_SIGNATURE_REJECTED_BY_PROVIDER, TRAVEL_RULE_REJECTED |
Recipe pairs
- Custodial USDC withdrawal vs non-custodial cosign approved/declined in Withdrawals.
- Fiat withdrawal (no custody flavor) in Withdrawals — Fiat withdrawal.
- Non-custodial pre-broadcast TR-reject in Travel Rule scenarios.
Common pitfalls
- In-flight 409 on non-custodial:
CONCURRENT_COSIGN_IN_FLIGHT. Resolve or wait for the prior payout. See the playbook. - 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: only pre-broadcast if you call
simulate/counterparty-webhookBEFOREsimulate/cosign. Once cosign is approved, the payout broadcasts and a counterparty-webhook reject becomes audit-only on the TR row. txHashconstraint onsimulate/confirm: thetxHashyou pass tosimulate/confirmdoes 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 arbitrary0xdeadbeef...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 are worth handling explicitly:
WALLET_CUSTODY_NOT_CLAIMED(409) — returned fromPOST /v2/customers/:id/walletswhen the customer is still custodial. Callclaim-non-custodialfirst; for legacy custodial customers (provisioned throughCRYPTO_WALLETbefore the non-custodial gate) submitPOST /v2/customers/:id/featureswith{ "type": "WALLET_CUSTODY_CONVERSION" }to convert.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 theCRYPTO_WALLETapplication before the non-custodial gate). UsePOST /v2/customers/:id/featureswith{ "type": "WALLET_CUSTODY_CONVERSION" }to convert them; multi-signer roster claim on existing custodial wallets will land in a follow-up.
See also
- Sandbox quickstart - end-to-end including the multi-signer
claim-non-custodialflow - Withdrawals - full state diagram and timing
- Travel Rule scenarios
CONCURRENT_COSIGN_IN_FLIGHTplaybookINVALID_ADDRESS_FORMATplaybookWALLET_CUSTODY_NOT_CLAIMEDreference