Test this flow in sandbox. Drive it end-to-end with simulated money and deterministic controls — start with the sandbox quickstart, then withdrawal simulation for this flow, and the cheat sheet for every magic value and simulate endpoint.
Overview
A payout is a client-initiated outbound transfer of funds out of a customer’s Conduit balance. UsePOST /v2/payouts to initiate and GET /v2/payouts/:id to track. Those two endpoint pages are the authoritative field-level reference for every request and response field; this guide covers the lifecycle, signing, cancellation, and failure handling around them.
Payouts are asynchronous. After submission the payout enters a pending state while compliance, Travel Rule exchange, and (for non-custodial wallets) co-signing complete. Subscribe to transaction.* webhooks for real-time state transitions.
Request body
A crypto payout (the source wallet is resolved fromcustomerId + the assetAmount code/chain):
virtualAccountId and a destination.type: "fiat" with a rail (fedwire, rtp, fednow, swift) and a recipient:
POST /v2/payouts endpoint reference. Two fields carry integration meaning worth calling out here: purpose selects the compliance requirement the payout must satisfy (see Payout requirements), and documents is required for every purpose except intercompany (a whitelisted recipient substitutes) and, by default, prefunding — documentation policy can still require documents on a prefunding payout above a configured amount, so handle 422 DOCUMENTATION_REQUIRED there too.
For a crypto payout, the destination address only has to be well-formed for its chain — a mixed-case EVM address must carry a correct EIP-55 checksum, otherwise the request returns 400 INVALID_ADDRESS_FORMAT. Any valid address is accepted as a destination.
Payout requirements
purpose selects the requirement the payout must satisfy before it is accepted:
intercompany— the recipient must be whitelisted for this customer, otherwise422 RECIPIENT_NOT_WHITELISTED. Register a bank recipient via whitelist recipients (it must reachregistered), or a crypto address via registered addresses. Supporting documents are not required for this purpose.prefunding— reserved for funding your own customers’ Conduit-issued accounts from a designated house account; the destination must resolve to one of your customers’ active issued accounts, otherwise a422 PREFUNDING_*error. Supporting documents are not required by default; a documentation policy may require them above a configured amount.- Any other purpose — at least one supporting document is required, otherwise
422 DOCUMENTATION_REQUIRED(the response listsacceptedDocumentTypes). Upload each document withPOST /v2/documentsusingpurpose=transaction_support, then pass itsdoc_*id indocuments. Document ids that don’t belong to your organization, or weren’t uploaded withpurpose=transaction_support, return400 DOCUMENT_IDS_NOT_FOUND. After acceptance the payout is held while the documents are reviewed; if the review is declined the payout ends asfailedand atransaction.rejectedwebhook fires withreasonCategory: "document_inadequate".
documents are not echoed back on GET /v2/payouts/:id, and a payout held for document review reads as a normal pending (there is no distinct in-review status). If the review declines the payout, GET /v2/payouts/:id returns status: "failed" with failureCode: "compliance_rejected" and a failureMessage, and the transaction.rejected webhook carries reasonCategory + acceptedDocumentTypes.
Response
The response shape is the same forPOST /v2/payouts and GET /v2/payouts/:id.
POST /v2/payouts and GET /v2/payouts/:id endpoint reference. Three of them need more than a field description to use correctly, so they have their own sections below: stage (see Progress: the stage field), failureCode (see Failures), and, for non-custodial payouts, requiresUserSignature and queuePosition (see Non-Custodial Crypto Withdrawals).
Progress: the stage field
While a payout is non-terminal (status: pending or processing), stage gives you a more specific
progress signal than status alone — useful for rails that can legitimately take days to settle.
stage is informational only. It does not replace requiresUserSignature, hasRfi, or failureCode
for deciding whether your integration needs to act — use those fields for that, not stage.
The set of
stage values may grow over time as new progress states are added; treat stage as
informational and handle an unrecognized value the same way you’d handle processing today.
Cancel a payout
transaction.cancelled webhook fires with cancellationReason: "client_cancelled".
Response: HTTP 200 with the payout in its current state. On success the cancel settles synchronously and
status is cancelled, cancellationReason is client_cancelled, and cancelledAt is populated; failureCode and failureMessage are omitted (cancellation is not a failure). In the rare case the cancel needs more than ~5 seconds to settle (cold-start), the response still returns 200 but the payout may still show pending or processing — the cancel was accepted and the final state will follow shortly. Poll GET /v2/payouts/:id for the terminal state. To dedupe your own retry, reuse the same idempotency-key (cached for 5 minutes) — the same response you got the first time replays. With a fresh idempotency-key, a payout you previously cancelled returns 200 again with the same cancelled shape; a payout that failed for any other reason returns 409 PAYOUT_NOT_CANCELLABLE.
Cancellable states:
Cancel is a state-based contract: any
pending payout can be cancelled until funds reach the rail. The practical window varies by payout type. Non-custodial crypto payouts park at the cosign gate awaiting the customer’s signature, so they stay cancellable for the lifetime of that gate — long enough to script a cancel against them. Fiat and custodial-crypto payouts move through pending quickly and hand off to the rail (bank or chain) inline once compliance clears, so by the time most integrators try to cancel they have already reached processing and return 409 PAYOUT_NOT_CANCELLABLE. A cancel issued early enough on a fiat or custodial-crypto payout can still succeed; the race is real but the window is narrow and not reliably scriptable.
Errors:
Non-Custodial Crypto Withdrawals
When the source wallet uses the non-custodial custody model, the payout requires the customer’s passkey approval before broadcast. Conduit cannot move non-custodial funds without the customer’s co-signature.How it works
- Submit
POST /v2/payoutsas usual. IfrequiresUserSignature: trueappears in the response, the payout is awaiting your customer’s signature. - The payout is screened for compliance and Travel Rule first. Once it clears,
transaction.processingfires (its status becomesprocessing), then thetransaction.awaiting_signaturewebhook fires when the payout is ready for the customer’s signature. Because screening runs before signing, a payout that fails compliance is rejected before this webhook is ever sent. The signature webhook carriesverificationUrl+expiresAton the payload itself. - Redirect the customer to the webhook’s
verificationUrlbeforeexpiresAt(default 15 minutes). - The customer approves with their passkey. Conduit broadcasts.
transaction.completedfires when the chain confirms.
This describes a wallet in the passkey signing mode — human signers approve on the verify page, so the webhook carries a
verificationUrl. A wallet in a programmatic signing mode receives a signingRequestId on the same transaction.awaiting_signature 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.POST /v2/payouts — response (non-custodial)
The requiresUserSignature field is available immediately on the POST response: it’s true from creation until the payout’s signatures are collected (or it terminates) — including while the payout is still clearing compliance before signing, when it reports stage: "under_review" and the verify link does not yet exist. It flips to false once the signatures are in (during settlement, stage: "settling") or the payout reaches a terminal status (completed / failed). The verify URL + expiry are not part of the response shape — they arrive on the transaction.awaiting_signature webhook, which fires only after compliance + Travel Rule screening clears. So a true value does not always mean the customer can sign right now. transaction.processing is only a progress signal (screening cleared, the payout is queuing/preparing to sign) and does not carry a verify link — route the customer to sign only after transaction.awaiting_signature, which supplies verificationUrl and expiresAt.
GET /v2/payouts/:id — response while awaiting signature
The signing step opens once the payout clears compliance and Travel Rule screening (those run before signing, so a compliance rejection stops the payout before any signature is requested). The verify URL + expiry are delivered on the transaction.awaiting_signature webhook, not on the GET response. The GET response continues to report requiresUserSignature: true while the payout is parked awaiting the customer’s signature:
verificationUrl + expiresAt arrive on the transaction.awaiting_signature webhook payload. expiresAt is an ISO 8601 UTC timestamp for the current signing attempt. If the signing window elapses before quorum, Conduit rebuilds the request with a fresh link and re-fires transaction.awaiting_signature with an incremented attempt. After the final rebuild attempt also elapses, the payout fails with failureCode: "user_signature_expired" and no funds are moved.
Webhook sequence
Failures
POST /v2/payouts returns synchronous errors only for validation and ID lookups. Once you receive 202 Accepted, the payout is in flight; most failures from that point arrive on the transaction.failed webhook. One exception: a payout declined at the document-review step (see Payout requirements) terminates on the transaction.rejected webhook instead — carrying reasonCategory + acceptedDocumentTypes — so subscribe to both. (GET /v2/payouts/:id then shows status: "failed" with failureCode: "compliance_rejected".)
Failure codes
Whentransaction.failed carries a failureCode, the failure has a known cause your integration can act on. Use the failureCode to decide what to show the customer and whether to retry.
These failures land at HTTP semantic
422 (the asynchronous equivalent: the request was well-formed and accepted, but the payout could not be completed).
Failures without a failureCode
If transaction.failed arrives with no failureCode, the payout could not be completed and the cause is not something your integration can act on. Treat the transaction as terminal. Funds, if any were reserved, are released. Contact support if the customer needs help understanding why.