When to use which custody model
New customers always provision multi-signer non-custodial wallets via the claim endpoint. The custodial column below applies only to customers that were provisioned through the legacyCRYPTO_WALLET application before the non-custodial gate; converting one of those to non-custodial uses the WALLET_CUSTODY_CONVERSION application.
| Custodial (legacy) | Non-custodial (single-signer, converted) | Multi-signer non-custodial | |
|---|---|---|---|
| Who holds signing material | Conduit | Customer (one passkey on a single end-user) | Customer team (one passkey per roster member) |
| Provisioned via | Legacy CRYPTO_WALLET application only | Approved WALLET_CUSTODY_CONVERSION application | POST /v2/customers/:id/wallets/claim-non-custodial |
| Payout signing | Auto | Single passkey via verify page | M-of-N roster, each stamps independently |
| Use when | Legacy customers only | Converting a legacy custodial customer | Default for every new customer |
Concepts
Roster. The ordered list of named signers attached to a customer’s non-custodial setup. Each signer has an email, a role (admin or signer), and exactly one passkey credential. Roster size and threshold are decoupled: a 5-member roster can require 2 stamps; a 2-member roster can require 2.
Threshold. The integer M in M-of-N. Every payout collects stamps until M are recorded, then auto-broadcasts. The threshold is set at claim-non-custodial time and can be adjusted later via the signing-quorum endpoints. See Signing thresholds for the constraints.
Admin vs signer. Both roles stamp payouts; only admins can change the roster (add, remove, promote, demote other members). The minimum-admin floor is enforced on every roster mutation so you can never lock yourself out.
Root quorum. Conduit’s underlying signing infrastructure binds admin signers to a root quorum that protects the sub-organization itself. Membership flips happen through a ceremony: a coordinated multi-step update that re-seats the root members. You see this surface as: a promote/demote operation returns 202 Accepted and the change shows up after the ceremony completes.
Ceremony. A short-lived background flow that re-seats the root quorum (on promote/demote) or rolls a roster (on add/remove). Ceremonies are sequential per customer; a second one queues if one is already in flight. Status is read via the internal ceremonies-list endpoint (GET /v2/internal/customers/:id/wallet-ceremonies).
Ghost-vote scrubbing. If a signer is removed while a payout is sitting at the quorum gate, that signer’s already-cast stamps are scrubbed from every in-flight payout for the customer. Affected payouts re-fire transaction.signature_collected with the new count; payouts that can no longer reach quorum on the new roster terminate with failureCode: "ROSTER_CHANGED" so clients can re-submit against the current roster.
Lifecycle endpoints
| Endpoint | Purpose |
|---|---|
POST /v2/customers/:customerId/wallets/claim-non-custodial | Provision the customer’s multi-signer setup. Required body: roster, signingThreshold. Returns 202 with a claimId; per-signer enrollment URLs fan out as wallet_signer.invited webhooks. |
POST /v2/customers/:customerId/wallet-signers | Add a signer to an existing roster. Returns the new signer row in PENDING_ACTIVATION. A wallet_signer.invited webhook delivers the verification URL. |
DELETE /v2/customers/:customerId/wallet-signers/:signerId | Remove a signer. Admins must be demoted first (returns 409 SIGNER_IS_ROOT_MEMBER otherwise). |
POST /v2/customers/:customerId/wallet-signers/:signerId/promote | Promote a signer to admin. Triggers a root-quorum ceremony. |
POST /v2/customers/:customerId/wallet-signers/:signerId/demote | Demote an admin back to signer. Triggers a root-quorum ceremony. |
Sandbox simulators
| Endpoint | Purpose |
|---|---|
POST /v2/sandbox/wallet-signers/:signerId/mark-enrolled | Marks a PENDING_ACTIVATION signer as enrolled. Fires wallet_signer.enrolled; once every roster member is enrolled, the wallet flips to ACTIVE and crypto_wallet.completed fires. |
POST /v2/sandbox/payouts/:id/simulate-stamp | Records one signer’s stamp against a multi-signer payout. Body carries walletSignerId and selection (APPROVED or REJECTED). Returns the post-stamp votesCollected / votesRequired. |
POST /v2/sandbox/customers/:customerId/simulate-reset-claim | Wipes the customer’s non-custodial setup (every signer, every wallet, the crypto-wallet feature flag) so a subsequent claim-non-custodial starts fresh. Refuses with 409 CLAIM_RESET_BLOCKED if open transactions, deposits, or pending signature approvals still reference the customer; terminalize them via simulate/terminal first. |
Webhooks
| Topic | Fires when |
|---|---|
wallet_signer.invited | One per roster member at claim time or POST /wallet-signers. Payload carries verificationUrl and expiresAt. |
wallet_signer.added | Co-emitted with wallet_signer.invited; carries the steady-state membership shape (role, credentialType, no URL). |
wallet_signer.enrolled | A signer completed their verification and is now ACTIVE. |
wallet_signer.removed | A signer was removed from the roster (direct removal only; demote does not emit removed). |
wallet_signer.promoted | A signer was promoted to admin after the root-quorum ceremony cleared. |
wallet_signer.demoted | An admin was demoted to signer after the root-quorum ceremony cleared. |
transaction.awaiting_user_signature | Payout parked at the quorum gate. Payload carries verificationUrl and expiresAt (the same URL also lives on per-signer wallet_signer.invited payloads for the enrollment phase). |
transaction.signature_collected | One stamp recorded. Payload carries votesCollected and votesRequired. May re-fire if a roster change scrubs a stamp. |
transaction.quorum_met | Quorum reached; payout proceeds to broadcast. |
transaction.failed with failureCode: "ROSTER_CHANGED" | A roster change scrubbed enough stamps that the payout can no longer reach quorum on the new roster. |
Error codes on the multi-signer surface
| Code | Surface | Meaning |
|---|---|---|
CUSTOMER_ALREADY_CUSTODIAL | claim-non-custodial | The customer already provisioned a custodial wallet via the legacy feature endpoint. |
CUSTOMER_ALREADY_NON_CUSTODIAL | claim-non-custodial | The customer already has a non-custodial wallet from a previous claim. |
CUSTOMER_KYB_INCOMPLETE | claim-non-custodial | The customer has not yet been KYB-approved. |
ROSTER_BELOW_MIN_ADMINS | claim-non-custodial, remove, demote | The proposed roster has fewer than the required minimum of admins. |
THRESHOLD_EXCEEDS_ROSTER | claim-non-custodial, add, remove | signingThreshold cannot exceed the number of ACTIVE signers. |
SIGNER_EMAIL_DUPLICATE | claim-non-custodial, add | Two roster members share an email. |
SIGNER_NOT_FOUND | remove, promote, demote, simulate-stamp | The signer id does not exist for the customer. |
SIGNER_NOT_ACTIVE | remove | Signer is still in PENDING_ACTIVATION (never enrolled). |
SIGNER_IS_ROOT_MEMBER | remove | Removing an admin directly would skip the root-quorum ceremony; demote first. |
SIGNER_ALREADY_ADMIN | promote | Target signer is already admin. |
SIGNER_NOT_ADMIN | demote | Target signer is not currently admin. |
CEREMONY_IN_FLIGHT | add, remove, promote, demote | A previous ceremony is still running for this customer; retry after a short backoff. |
WOULD_BREAK_MIN_ADMINS | remove, demote | The change would drop the admin count below the minimum floor. |
QUORUM_THRESHOLD_EXCEEDS_SIGNERS | quorum endpoints | The requested threshold is greater than the active signer count. |
QUORUM_WALLET_OVERRIDE_CANNOT_RAISE | per-wallet quorum override | A per-wallet override cannot exceed the customer-default threshold. |
CLAIM_RESET_BLOCKED | simulate-reset-claim | Open transactions, deposits, or pending signature approvals still reference the customer; terminalize them first. |
PROVIDER_ACCOUNT_NOT_FOUND | simulate-reset-claim | The customer has no non-custodial setup to reset. |