Skip to main content
This guide follows a single payout from a non-custodial wallet through its whole life: who signs, what gates it passes, which webhook tells you where it is, and what to do when something goes wrong. Each step links down to the concept page that owns the detail and out to the error playbook for that step’s failures. Need the mental model for one piece — the roster, the threshold, the verify page? Start at Multi-signer wallets. Need the full payload schema for an endpoint? Follow the links to the reference. This page assumes your customer is already KYB-approved.
Non-custodial means the customer holds the signing material. A payout cannot broadcast until enough of the customer’s signers approve it on the Conduit-hosted verify page — there is no code path in which Conduit moves the funds alone. See Non-Custodial Wallets.

The lifecycle at a glance

PhaseYou holdYou callYou hearRead more
1. Provision signers(no wallet yet)POST /v2/customers/:id/wallets/claim-non-custodialwallet_signer.invited, wallet_signer.addedMulti-signer wallets
2. Enroll the rostersigners PENDING_ACTIVATION(signers enroll out-of-band)wallet_signer.enrolled, then crypto_wallet.completedMulti-signer wallets
3. Set the thresholdwallet ACTIVEPUT /v2/customers/:id/signing-quorumSigning thresholds
4. Whitelist the destinationwallet ACTIVEPOST /v2/customers/:id/wallets/registered-addresses— (synchronous)Registered Addresses
5. Initiate the payoutpayout pendingPOST /v2/payoutstransaction.created, transaction.awaiting_user_signatureNon-Custodial Wallets
6. Collect signaturespayout pending(signers approve on the verify page)transaction.signature_collected, transaction.quorum_metMulti-signer wallets
7. Settlepayout pendingcompletedtransaction.completedNon-Custodial Wallets
Payout status only ever takes the values pending, processing, completed, failed, and cancelled. The signing handshake happens while status is pending. The requiresUserSignature flag — not the status — tells you a signature is outstanding.

Step 1 — Provision the signers

State: the customer has no non-custodial wallet yet. Advance: call POST /v2/customers/:customerId/wallets/claim-non-custodial with the roster (each signer’s email and role) and the signingThreshold. It returns 202 Accepted with a claimId. Provisioning runs in the background. Do not poll the claimId — it is a correlation receipt, not a status handle; you wait for the webhooks (wallet_signer.*, then crypto_wallet.completed). Webhooks: for each roster member, a pair fires together — wallet_signer.added (the steady-state row, status PENDING_ACTIVATION) and wallet_signer.invited, which carries the per-signer verificationUrl and expiresAt. Send each invited signer’s URL to that person out of band. What can go wrong: the claim is rejected synchronously when the roster or threshold is malformed:
CodeMeaning
CUSTOMER_KYB_INCOMPLETEThe customer is not yet KYB-approved.
CUSTOMER_ALREADY_NON_CUSTODIAL / CUSTOMER_ALREADY_CUSTODIALThe customer already has a wallet from a previous claim or the legacy custodial feature.
ROSTER_BELOW_MIN_ADMINSFewer than the minimum two admins on the roster.
THRESHOLD_EXCEEDS_ROSTERsigningThreshold is greater than the number of signers.
SIGNER_EMAIL_DUPLICATETwo roster members share an email.
See Signing thresholds for how the threshold and minimum-admin constraints compose.

Step 2 — Enroll the roster

State: each signer is PENDING_ACTIVATION. The wallet is not yet usable. Advance: no API call for you here — each signer opens their verificationUrl and registers a passkey before expiresAt. Do not poll. Wait for the webhooks. Webhooks: wallet_signer.enrolled fires as each signer completes enrollment and flips to ACTIVE. Once every roster member is enrolled, the wallet becomes usable and crypto_wallet.completed fires carrying the wallet IDs — your signal the customer can now receive and send funds. The wallet only becomes ACTIVE after every invited member enrolls — independent of the signing threshold. If one invited signer never enrolls, the wallet never activates, even if the threshold could otherwise be met. What can go wrong: an invitation not used before expiresAt auto-expires; re-invite the signer with POST /v2/customers/:customerId/wallet-signers. Adding, removing, promoting, or demoting a signer runs through a sequential ceremony. A second roster change while one is in flight returns 409 CEREMONY_IN_FLIGHT; retry after a short backoff.

Step 3 — Set or adjust the signing threshold

State: wallet ACTIVE. Advance: the threshold you set at claim time governs every payout. To change it, call PUT /v2/customers/:customerId/signing-quorum with the new threshold. To require more signatures on one high-value wallet, set a per-wallet override with PUT /v2/wallets/:walletId/signing-quorum (an override may only lower the threshold relative to the customer default). Read the current value at GET /v2/customers/:customerId/signing-quorum. Webhooks: none — the threshold is configuration, not a payout event. What can go wrong:
CodeMeaning
QUORUM_THRESHOLD_EXCEEDS_SIGNERSThe requested threshold is greater than the active signer count.
QUORUM_WALLET_OVERRIDE_CANNOT_RAISEA per-wallet override tried to raise the threshold above the customer default.
The full constraint model — M-of-N, the minimum-admin floor, and the one-way override rule — lives in Signing thresholds.

Step 4 — Whitelist the destination (intercompany payouts only)

State: wallet ACTIVE. This step applies only to payouts you send with purpose: INTERCOMPANY. For any other purpose, skip it — the inline recipient is accepted without a prior whitelist entry. Advance: call POST /v2/customers/:customerId/wallets/registered-addresses with the destination chain and address. Registration is synchronous: a successful POST returns the address already in REGISTERED status. An INTERCOMPANY payout then matches its destination.recipient.address against the registered entry by chain and address. Webhooks: none for crypto registered addresses — they register synchronously. What can go wrong: registration can be screened and suspended on the spot, returning 409 REGISTERED_ADDRESS_SUSPENDED (do not retry; contact Conduit). The failure most integrators hit comes later, at payout time: an INTERCOMPANY payout to an address with no REGISTERED entry returns 422 — see RECIPIENT_NOT_WHITELISTED. See Registered Addresses for the custody-type discriminator and Travel Rule disclosure fields — the Travel Rule being the requirement to exchange originator and beneficiary information on transfers.

Step 5 — Initiate the payout

State: about to create the payout. Advance: call POST /v2/payouts (requires the idempotency-key header). It returns 202 Accepted with the payout in status: "pending" and its id. Because the source wallet is non-custodial, the response also carries requiresUserSignature: true — your frontend can prepare to route signers at once, without waiting for the webhook. See the Payouts reference for the full request body and fields. Webhooks: transaction.created fires on initiation. Compliance and Travel Rule checks then run in the background with no client action; the customer is never asked to sign a payout that has not passed screening. When screening clears, transaction.awaiting_user_signature fires once for the payout, carrying the shared verificationUrl, expiresAt, and requiredApprovals. What can go wrong (synchronously, on the POST):
CodePlaybook
INVALID_ADDRESS_FORMATInvalid address format
RECIPIENT_NOT_WHITELISTEDRecipient not whitelisted
CONCURRENT_COSIGN_IN_FLIGHTConcurrent cosign in flight — the same wallet already has a payout awaiting signature on the same chain
INSUFFICIENT_FUNDSThe wallet’s available balance cannot cover the payout.
CONCURRENT_COSIGN_IN_FLIGHT is the sequencing trap: signing on a wallet+chain is single-file. If GET /v2/payouts/:id reports a non-zero queuePosition, an earlier payout is signing ahead of this one.

Step 6 — The signing handshake

State: status: "pending", requiresUserSignature: true. The payout is parked at the quorum gate. Advance: route each signer to the verificationUrl from the transaction.awaiting_user_signature payload (the URL and expiresAt arrive only on the webhook — they are not on GET /v2/payouts/:id, so persist them). Each signer approves on the Conduit-hosted verify page with their passkey. They stamp independently until the threshold M is reached. For what the signer sees and what you host versus what Conduit hosts, see Non-Custodial Wallets → Verify Page. Webhooks: transaction.signature_collected fires once per stamp, carrying collected and required — drive a progress UI off these. When collected >= required, transaction.quorum_met fires, Conduit applies its own signature, and the payout proceeds to broadcast. What can go wrong:
Failure (failureCode on transaction.failed)Playbook
USER_SIGNATURE_TIMEOUTUser signature timeout — no signer approved before expiresAt (default 15 minutes)
USER_SIGNATURE_DECLINEDUser signature declined — a signer declined on the verify page
USER_SIGNATURE_REJECTED_BY_PROVIDERUser signature rejected by provider — a passkey approval could not be accepted
CRYPTO_WALLET_MISCONFIGUREDCrypto wallet misconfigured — the wallet cannot be signed for
ROSTER_CHANGEDA signer was removed mid-flight and the payout can no longer reach quorum — re-submit against the current roster
In every case no funds move and the payout is terminal — submit a new payout to retry. If a signer is removed while a payout sits at the gate, that signer’s stamps are scrubbed and transaction.signature_collected re-fires with the lowered count. See ghost-vote scrubbing in Multi-signer wallets.

Step 7 — Settlement and terminal state

State: quorum met, both Conduit and the customer signatures applied, broadcast underway. Advance: nothing to do — wait for the chain to confirm. Webhooks: transaction.completed fires when the chain confirms; the on-chain settlement reference is on destination.external_crypto.txHash. The payout’s status is now completed and requiresUserSignature is false. This is the terminal success state. A payout can instead reach a terminal failure (transaction.failed, status failed) for any of the Step 6 reasons, or terminal cancellation (transaction.cancelled, status cancelled) if you called POST /v2/payouts/:id/cancel before broadcast began. A cancellation is not a failure — it releases the reserved balance back to available and carries no failureCode. Once broadcast has begun, cancel returns 409 PAYOUT_NOT_CANCELLABLE.

Terminal-state summary

Terminal statusWebhookFunds moved?Next step
completedtransaction.completedYes — settled on-chainReconcile on txHash
failedtransaction.failedNoBranch on failureCode; submit a new payout
cancelledtransaction.cancelledNo — reserved balance releasedRe-submit when ready
For the full payload schema of every event named here, see the Webhooks reference.