Overview
For sandbox lifecycle prose + copy-paste recipes (cosign approved and declined), seewithdrawals.mdx Step 4.
Conduit supports two custody models for crypto wallets:
In the non-custodial model, every customer has a dedicated key set that requires two cryptographic signatures to move funds: one from Conduit, and one from the customer. The customer’s signature is generated on-device using a WebAuthn passkey — Conduit never sees the private key. Conduit alone cannot move non-custodial funds.
Signing modes
The customer’s signature doesn’t have to come from a human. Every customer has a signing mode, configured by Conduit out of band as an organization-wide default with per-customer overrides (the customer-level setting wins) — ask your Conduit representative; there is no API to set it.
Everything below on this page — the verify page, WebAuthn,
verificationUrl — describes the passkey-required mode. In a programmatic mode, machine signers seat active at claim time with no enrollment step, the transaction.awaiting_signature webhook carries a signingRequestId (plus a verificationUrl only when the roster has an active passkey signer, who may approve on the verify page alongside the machine signers), and your backend reads the request via GET /v2/signing-requests/{id} and resolves it via POST /v2/signing-requests/{id}/approve or .../reject. Conduit’s compliance co-approval is still always required — a machine can never move money alone. See Programmatic payout signing.
Enabling crypto for a customer
Before a customer can claim a non-custodial wallet roster, they need an approvedCRYPTO_WALLET feature on file:
Two-signature model
Moving funds from a non-custodial wallet always requires both:- The customer’s signature — produced when the customer approves on the Conduit-hosted verify page using their passkey (Face ID, Touch ID, or a hardware security key).
- Conduit’s compliance signature — applied automatically once the customer quorum is collected. Compliance and Travel Rule screening already ran before the signature was requested, so this final stamp applies that cleared result rather than re-deciding it — a payout that passed screening is not re-screened after signing. Conduit cannot broadcast without this stamp.
Withdrawal Flow
A non-custodial payout is screened for compliance and Travel Rule first. Only once it clears does Conduit request the customer’s signatures (transaction.processing fires and the payout’s status becomes processing). After the quorum is collected, Conduit adds its own compliance stamp and broadcasts. Because screening runs before signing, a payout that fails compliance is rejected before any signer is asked to approve — the customer never signs a payment that then fails screening.
transaction.failed fires with the matching failureCode (see Failures below).
Verify Page
The verify page is a Conduit-hosted UI served athttps://app.conduit.financial/verify/<token>. It is:
- Branding-aware — displays your organization’s logo and colors.
- Information-rich — shows the transfer asset, amount, and destination address so the customer knows exactly what they are approving.
- WebAuthn-native — uses the customer’s registered passkey (no password, no SMS OTP).
Flow
- Your backend receives
transaction.awaiting_signaturewithverificationUrlon the webhook payload. - Route your customer to that URL (deep link, redirect, or in-app WebView).
- The customer taps “Approve” — their device presents a biometric prompt (Face ID, Touch ID, or hardware key).
- Conduit receives the passkey approval and resumes the payout.
- The customer sees a confirmation screen. The payout proceeds to broadcast.
Branding Customization
The verify page respects your organization’s branding configuration (logo URL, primary color). Contact Conduit support to configure or update your branding.Timeout and Decline Behavior
The signing deadline for each attempt is the
expiresAt delivered on the transaction.awaiting_signature event — always drive your timers off that value, never a fixed duration. The window is not 15 minutes: in production it runs up to roughly 23 hours on EVM chains and 12 hours on Tron. The link-expiry (verificationUrl) and the signing timer are separate — the signing timer is the one that governs the payout, and it is the expiresAt on the webhook. This window and its rebuild behaviour are the same for a wallet in a programmatic signing mode: a machine request also carries an expiresAt, is rebuilt with an incremented attempt (a new signingRequestId) when the window closes, and fails with user_signature_expired only after the final attempt. Always treat the latest attempt as authoritative and discard prior links.
Webhook Integration
Subscribe totransaction.awaiting_signature to know when a non-custodial payout is awaiting approval. See the Webhooks reference for the full payload schema.
The
verificationUrl on this webhook applies to a wallet in the passkey signing mode — the human-signer, verify-page model this page describes. A wallet in a programmatic signing mode carries a signingRequestId on the same webhook — plus an optional verificationUrl when its roster has an active passkey signer who may also approve on the verify page; a machine integration reads the request via GET /v2/signing-requests/{signingRequestId} and resolves it via POST .../approve or .../reject. See Machine-signer stamping.GET /v2/payouts/:id. Persist the webhook payload (or its essential fields) so you can surface the verify URL to the customer on demand.
GET /v2/payouts/:id Response
While a payout is awaiting the customer’s signature, the response continues to report requiresUserSignature: true and, if the payout is queued behind earlier signing work on the same wallet+chain, a queuePosition:
queuePosition is the payout’s slot in the per-(wallet, chain) signing queue. Waiting payouts (queued behind a payout already in the signing step) expose this as a positive integer. The active payout (currently being signed) has the field absent from the response. The field is also absent once the payout advances past the queue gate or reaches a terminal status. The value is eventual — it may not appear on the initial POST response; read it via GET /v2/payouts/:id.
requiresUserSignature is a present-tense gate: true while the payout is awaiting the end-user’s signature, false once status reaches completed, failed, or cancelled. It is also present on the POST /v2/payouts response (derived from the wallet’s custody model at submission) so your frontend can immediately prepare the user-routing flow without waiting for the webhook. After the response goes terminal, expect false regardless of the source wallet’s custody model — don’t keep waiting on a settled row.
Sandbox Testing
In the sandbox environment, the real signing flow is bypassed. Resolve the cosign gate via one of two endpoints. They produce the same outcome a real customer action on the verify page would, and the payout reachescompleted or failed automatically — no follow-up simulate/confirm is required.
Simulate endpoints
Recommended: resolve by payout ID — most integrators should use this variant. Drive any specific payout end-to-end keyed by theid returned from POST /v2/payouts. Each call casts one signer’s vote: omit signerId and repeated calls auto-advance through the roster one signer at a time until the threshold is met, or pass a signerId to cast a specific signer’s vote. A single declined vote terminalizes the whole quorum as declined.
200 with the payout at its current state; signerId is optional. Calling again after the gate has already resolved is safe — it returns the payout at its current state rather than an error.
Per-signer variant — records one named signer’s stamp against a payout, so a test can walk quorum progress signer by signer (while the signing request is still pending, re-stamping by the same signer is idempotent):
200 with the payout at its current state; 404 if the payout is not awaiting a signature or the signer is not on the wallet’s roster. Note the contrast with simulate/cosign above: once the quorum has resolved, a repeat call here returns 404 NOT_IN_AWAITING_SIGNATURE rather than the payout’s current state.
See the Multi-signer wallets recipe and the Custody sandbox guide for full walkthroughs.
Failures
OncePOST /v2/payouts returns 202 Accepted, any failure during the signing flow arrives on the transaction.failed webhook. When the failure has a cause your integration can act on, the payload carries a failureCode:
The webhook
reason field is a human-readable version of the failureCode. The same failureCode is surfaced on GET /v2/transactions/:id and GET /v2/payouts/:id, so a fresh GET reconciles 1:1 with the webhook.
If transaction.failed arrives without a failureCode, the payout could not be completed and the cause is not something your integration can act on. Treat the transaction as terminal and contact support if the customer needs help understanding why.