Overview
For sandbox lifecycle prose + copy-paste recipes (cosign approved and declined), seewithdrawals.mdx Step 4.
Conduit supports two custody models for crypto wallets:
| Model | Who controls the key | How funds are moved |
|---|---|---|
| Custodial | Conduit | Conduit signs and broadcasts autonomously |
| Non-custodial | Customer (end-user passkey) | Conduit and the customer co-sign; Conduit broadcasts after both signatures land |
Two-signature model
Moving funds from a non-custodial wallet always requires both:- Conduit’s signature — applied automatically once compliance and Travel Rule checks pass.
- 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).
Withdrawal Flow
Compliance and Travel Rule data exchange complete before the customer is prompted. The customer is never asked to sign a transaction that has not already passed screening.transaction.failed fires with the matching failureCode (see Failures below).
Verify Page
The verify page is a Conduit-hosted UI served athttps://web.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_user_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
| Scenario | What happens | Customer-facing message |
|---|---|---|
| Customer approves within the signing window | Withdrawal proceeds to broadcast | Confirmation screen on the verify page |
| Customer does not approve within the signing window (default 15 min) | Payout fails; no funds moved; transaction.failed fires with failureCode: "USER_SIGNATURE_TIMEOUT" | ”The signing window expired. Start a new payout when the customer is ready.” |
| Customer clicks “Decline” on the verify page | Payout fails; no funds moved; transaction.failed fires with failureCode: "USER_SIGNATURE_DECLINED" | ”The customer declined the payout.” |
| Invalid passkey stamp (retryable) | Verify page shows an error; customer can retry | Verify page error — same session, no new payout needed |
Webhook Integration
Subscribe totransaction.awaiting_user_signature to know when a non-custodial payout is awaiting approval. See the Webhooks reference for the full payload schema.
Recommended flow on your backend:
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 (0 = head of queue / signing now). It is absent once the payout is no longer on the user-signature path or has reached a terminal status.
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 sandbox mode (staging-sandbox / production-sandbox), 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 reaches completed 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:
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:
failureCode | What happened | What to do |
|---|---|---|
USER_SIGNATURE_TIMEOUT | The customer did not approve the payout before the signing window expired (default 15 minutes). | No funds were moved. Submit a new payout when the customer is ready to sign. |
USER_SIGNATURE_DECLINED | The customer declined the payout from the approval page. | No funds were moved. Submit a new payout if the decline was unintentional. |
USER_SIGNATURE_REJECTED_BY_PROVIDER | The customer’s passkey approval could not be accepted. | No funds were moved. Submit a new payout. If the same customer or wallet hits this repeatedly, contact support. |
CRYPTO_WALLET_MISCONFIGURED | The wallet’s configuration prevents Conduit from moving funds from it. | No funds were moved. Conduit is investigating automatically. Contact support if the wallet is needed for a time-sensitive payout. |
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.