Overview
This page walks the multi-signer non-custodial flow as it works in sandbox: a customer claims non-custodial control, the roster reaches its signing quorum, deposit addresses become available, and a payout collects a stamp from each required signer before broadcasting. The same six webhook topics fire in live — the only sandbox-specific behavior is auto-activation (described under Sandbox vs live caveats).Pick your roster to match the signing mode
The roster shape you claim with depends on the customer’s signing mode, which Conduit sets per organization. Send the wrong shape and the claim is rejected.A new sandbox organization defaults to Machine members need no enrollment — there is no
programmatic_unattended. The mode limits api_key members only; passkeys count toward signingThreshold in every mode, so an all-passkey roster is valid too. programmatic_unattended (the sandbox default) permits api_key admins, so the shortest path is a machine-signer roster — at least two api_key admins, each carrying a compressed publicKey (P-256 by default, or a secp256k1 key with signatureScheme: "secp256k1" / "secp256k1_eip191"), with signingThreshold reached by those keys. (The stricter programmatic mode rejects api_key admins with 422 SIGNING_MODE_ROSTER_INVALID: there the machine members take role: "signer" and admins are passkeys.)wallet_signer.invited / wallet_signer.enrolled cycle (steps 2–5 below). The claim returns status: "provisioning", and activation then completes on its own and claim.completed fires; await that webhook (or poll GET /v2/customers/:id/wallets until each chain is active) before using the wallets. You then sign each payout with a machine stamp at POST /v2/signing-requests/:id/approve; the full generate-key-and-stamp loop is in Machine-signer stamping. The customer’s mode surfaces publicly as signingMode on the transaction.awaiting_signature webhook.The 6-step passkey recipe below works in every signing mode. It is the only recipe for a customer set to passkey_required (Conduit sets this; older organizations default to it), because that mode refuses any api_key member.Prerequisites
- The customer must be KYB-approved (
customer.activatedfired). - Your organization has a webhook endpoint registered and reachable. The whole flow is webhook-driven; without an endpoint you will never see step 2 onward.
Before you claim: activate the CRYPTO_WALLET feature
claim-non-custodial requires an approved CRYPTO_WALLET feature on the customer. The pattern is:
status: "approved" and you can claim immediately. To rehearse the live review path, drive the application terminal yourself:
claim-non-custodial before the feature is approved returns 422 CRYPTO_FEATURE_NOT_APPROVED. Calling it from a country on Conduit’s crypto-restricted list returns 422 CRYPTO_NOT_AVAILABLE_IN_JURISDICTION at the feature-application step (the claim never runs).
The 6-step recipe
This recipe is the passkey path — each signer enrols a passkey and stamps on the verify page. It works in every signing mode. For aprogrammatic_unattended customer (the sandbox default), the machine-signer roster above is the shorter path: sign via Machine-signer stamping; that roster needs no enrollment (its wallets activate on their own after the claim — await claim.completed), so steps 2–5 do not apply.
1
Claim non-custodial control
POST /v2/customers/:customerId/wallets/claim-non-custodial with a roster, a signing threshold, and an optional chains array. Omit chains to provision a wallet on the default set of supported chains in one claim; pass it to choose the chains, including ones outside the default set such as solana (see Crypto Wallets). The customer must have an approved CRYPTO_WALLET feature (see above).202 Accepted:2
Receive one wallet_signer.invited webhook per roster member
Each invited signer gets their own webhook with a per-user
verificationUrl. URLs are scoped to one signer and expire at expiresAt.3
Distribute the verification URLs out-of-band
Send each signer their own
verificationUrl through your product’s notification channel — email, Slack, in-app — whatever you use to reach end users. Conduit does not deliver these URLs to signers directly.Treat the URL like a magic-link credential: one signer, one URL, do not share across the roster.4
Receive one wallet_signer.enrolled webhook per signer
When a signer opens their URL and completes enrollment, you receive:Track these against the
walletSignerIds you saw in step 2 to know when the last signer has enrolled.5
Wallets become usable (sandbox auto-activation)
Once every roster member has enrolled, sandbox auto-activates the customer’s wallets and fires A customer-level
claim.completed, carrying the claimId and the activated walletIds:crypto_wallet.completed (carrying customerId only, no wallet IDs) also fires. Deposit addresses are now available via GET /v2/customers/:customerId/wallets. The customer can receive funds and submit payouts.6
Submit a payout — stamps collect over webhook
POST /v2/payouts returns 202 { id: "txn_...", status: "pending" }. The payout is screened for compliance + Travel Rule before any signer is asked to sign, so a payout that fails screening is rejected before the roster ever sees it. Once screening clears the status flips to processing and the payout drives these event topics:1. transaction.processing — fires once when compliance + Travel Rule clear and the payout enters the signing queue. Progress signal only; it carries no verificationUrl, so do not route signers off this event.2. transaction.awaiting_signature — fires once when the payout parks at the cosign gate. In the passkey signing mode (shown here) the verificationUrl is a single shared approval page; the roster members signed-in there each stamp the payout with their passkey. A wallet in a programmatic signing mode carries a signingRequestId — plus an optional verificationUrl when its roster has an active passkey signer who may also approve on the verify page — see Machine-signer stamping.transaction.signature_collected — fires once per signer stamp. Drive a progress UI off collected / required.transaction.quorum_met — fires once when collected >= required. Compliance already passed before signing; once quorum is met, Conduit casts its final compliance approval to complete the transfer — applying the screening result already on file, not re-screening after signing.transaction.completed (or transaction.failed) — fires once when the payout reaches a terminal state. In sandbox it fires after the payout reaches real testnet finality, which takes minutes. transaction.failed carries a failureCode your integration branches on — see Failure cases.How compliance works in sandbox. Compliance + Travel Rule screening runs before the signers are asked to sign; in sandbox it auto-passes, so the payout flips to
processing and reaches the co-signing gate immediately. Once the signers reach the configured threshold, the sandbox casts Conduit’s final compliance approval and the payout broadcasts on the testnet. In live, the pre-signing screening runs the real sanctions + travel-rule checks; after quorum Conduit casts its final compliance approval to complete the transfer, applying the screening result already on file rather than re-screening.Failure cases
A payout that does not complete firestransaction.failed with one of the codes below.
See the error reference for the full catalog and resolution playbooks.
Roster lifecycle ceremonies
Once the 6-step recipe above ships a working multi-signer wallet, three lifecycle ceremonies on the roster itself become testable. All three are sandbox-runnable; the underlying root-quorum ceremony is identical to production.Add a fourth signer mid-life
POST /v2/customers/{customerId}/wallet-signers provisions a new signer on an active roster. The threshold is unchanged; only the eligible-stamper pool grows.
wallet_signer.invited(new signer; payload carriesverificationUrl)wallet_signer.added(co-emitted)wallet_signer.enrolled(after the signer registers a passkey)
Promote a signer into the root quorum (seat swap)
POST /v2/customers/{customerId}/wallet-signers/{signerId}/promote seats a signer (or a non-root admin) in the root quorum; .../demote unseats a root admin. Each call runs a root-quorum ceremony in the background.
The root quorum holds a fixed two customer seats. When both are filled — as they are on the two-admin roster from the recipe above — promoting a new admin into it is a seat swap: the promote call names the current root admin to unseat via demoteSignerId, and one ceremony does both.
- Promoting without
demoteSignerIdwhile both root seats are filled returns409 ROOT_AT_CAPACITY_SWAP_REQUIRED. Retry with ademoteSignerId. demoteSignerIdmust name a current root-quorum admin, else409 DEMOTE_TARGET_NOT_IN_ROOT.- A swap that would leave fewer than 2 admins on the roster returns
409 SWAP_WOULD_BREAK_F12. - Running a second promote/demote before the first ceremony completes returns
409 CEREMONY_IN_FLIGHT. Retry after a short backoff. - Demoting an admin that would drop the admin count below the floor returns
409 WOULD_BREAK_MIN_ADMINS(at the default two root seats, demoting either root admin hits this).
Parked ceremony approvals
Some roster changes park awaiting an existing admin’s approval before the ceremony runs: thewallet_ceremony.awaiting_admin_approval webhook fires carrying the admin link in its adminVerificationUrl field (webhook payloads have no urlAudience field — that field belongs to the POST /wallet-signers response). An admin opens that link and approves with their passkey, in sandbox and in production. The parked ceremony then resumes; a decline rejects it.
Ghost-vote scrubbing: signer removed mid-payout
DELETE /v2/customers/{customerId}/wallet-signers/{signerId} removes a signer. It returns 204 No Content; the removal outcome is disclosed additively in the response headers — X-Conduit-Ceremony-Status (pending_removal while the roster-remove ceremony awaits admin approval, or removed once fully removed) and X-Conduit-Ceremony-Id (the ceremony id; omitted once terminally removed). Retrying the same DELETE is idempotent. If the signer had already stamped an in-flight payout, the stamp is scrubbed from every affected payout. Payouts that can no longer reach quorum on the new roster terminate failed with failureCode: "roster_changed".
wallet_signer.promoted(signer D, seated into the root quorum)wallet_signer.demoted(signer A, unseated by the same swap)wallet_signer.removed(signer A)transaction.signature_collected(collected: 0) — re-fires after the scrubtransaction.failedwithfailureCode: "roster_changed"— payout cannot reach quorum on the new roster
Sandbox vs live caveats
Multi-signer wallets run in production, and the recipe on this page is the recipe you integrate against there. A passkey signer enrolls and approves with a real device-bound passkey in sandbox and in live, so that path needs a person at a device. A machine-signer roster runs without one.
- Wallet activation is automatic in sandbox, manual in live. When the last signer enrolls in sandbox,
crypto_wallet.completedfires immediately. In live, activation runs an additional governance ceremony before the wallet becomes usable. - Roster and enrollment ceremonies can wait for an admin approval. In production, adding a signer, removing a signer and attaching a passkey each need one business-admin stamp, so the call answers
202 Acceptedwith a ceremony id and an admin verification URL. Promotion, demotion and signing-quorum changes wait for an approval in sandbox too. When a ceremony parks, an admin opensadminVerificationUrland approves with their passkey. - Chain finality is real. After
quorum_met, the payout broadcasts on a real testnet andtransaction.completedfires at finality, which takes minutes. ItstxHashresolves on the network’s explorer.
What changes in live
The contract you integrate against is the same. The pieces underneath swap out:- The real compliance pipeline (sanctions screening + travel rule) replaces the auto-pass stamp. Some payouts will be rejected here that always passed in sandbox.
- A real activation ceremony replaces the sandbox auto-activate hook.
crypto_wallet.completeddoes not fire the instant the last signer enrolls. - The per-(wallet, chain) signing queue runs the same in sandbox as in live: a queued payout carries a positive
queuePositiononGET /v2/payouts/:idand on thetransaction.queue_position_changedwebhook in both.
Sandbox-only levers
POST /v2/sandbox/customers/:customerId/simulate-reset-claim tears down the customer’s wallet setup so a claim can be re-run. A production wallet claim is not reversible.
The signing and enrollment levers (wallet-signers/:signerId/mark-enrolled, payouts/:id/simulate-stamp, payouts/:id/simulate/cosign, orders/:id/simulate/cosign, verifications/:token/simulate/ceremony-stamp, customers/:customerId/wallet-ceremonies/simulate-approval-gate) return 409 SANDBOX_SIGNING_SIMULATION_UNAVAILABLE on the sandbox. Enroll and approve with a real passkey, or use a machine-signer roster.
See also
- Multi-signer wallets — the conceptual mental model
- Withdrawals — full state diagram for the payout lifecycle
- Webhooks reference — every topic with full payload schema
- Error codes — failure-code resolution playbooks
- Sandbox overview