Overview
This page walks the multi-signer non-custodial flow as it works in sandbox: a customer claims non-custodial control, each signer enrolls a passkey, 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).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.
Headless sandbox testing. The webhook-driven flow above assumes signers complete enrollment + payout approval through the verification portal. Two sandbox-only shortcut endpoints let you script the whole loop without a browser:These endpoints exist in sandbox only — they have no live counterpart.
POST /v2/sandbox/wallet-signers/:signerId/mark-enrolled— bypass passkey enrollment and mark a signer enrolled. Auto-activation fires once every roster member is marked.POST /v2/sandbox/payouts/:payoutId/simulate-stamp— stamp a payout on behalf of a signer without driving the approval page.
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
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 every supported chain in one claim; pass it to narrow the set (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 the chain-confirm autopilot resolves this within ~5 seconds of quorum_met. 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, so transaction.quorum_met is followed within milliseconds by transaction.completed. 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(aftermark-enrolled)
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). In production an admin opens that link and approves with their passkey. In sandbox, stamp it headlessly using the verification token from that URL:
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 are currently available in sandbox only. The live integration is still in progress; live builds reject new multi-signer methods with a sandbox-only stub for now.
- 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. - Passkey enrollment uses synthetic credentials in sandbox. The sandbox verification flow goes through the same browser passkey UX as live — your test signers will see the same OS prompt — but the credentials it produces are sandbox-only and never authorize real funds.
- Chain confirmation autopilots in sandbox. After
quorum_met, sandbox resolvestransaction.completedwithin ~5 seconds without a real chain broadcast. Live waits for real chain finality. - No sandbox payout ever touches a real chain. Destination addresses, hashes, and signatures in sandbox are isolated from mainnet. A
txHashon a sandboxtransaction.completedis shape-valid hex but not lookupable on-chain.
What changes in live
The contract you integrate against is the same. The pieces underneath swap out:- The real custody provider replaces the sandbox mock.
- Real WebAuthn passkeys replace synthetic credentials. The headless
mark-enrolledendpoint has no live counterpart — signers must complete browser passkey enrollment. - 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
queuePositionfield on signing-queue webhooks is populated by the real per-(wallet, chain) counter in live; in sandbox it is omitted.
See also
- Multi-signer wallets — the conceptual mental model
- Custodial vs non-custodial — the side-by-side comparison
- Withdrawals — full state diagram for the payout lifecycle
- Webhooks reference — every topic with full payload schema
- Error codes — failure-code resolution playbooks
- Sandbox overview