Skip to main content
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. Use POST /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. If you resell Conduit, a fiat payout can carry your own margin — see Take your own margin. A crypto payout cannot. To charge on money arriving instead, see Charge a fee on a deposit.

Request body

A crypto payout (the source wallet defaults to the customer’s oldest active wallet on the assetAmount chain; pass sourceWalletId to spend from a specific one):
To move crypto between two wallets of the same customer, send a wallet destination instead of a recipient. The transfer is a real on-chain send, so it settles like any other payout and the receiving wallet is credited when it confirms. It requires purpose: "intercompany", an assetAmount.chain, and a destination wallet that differs from the source. No registered address and no whitelist entry apply, because there is no counterparty:
A destination wallet that is not an active wallet of that customer on the asset’s chain returns 422 INVALID_TRANSFER_PAIR. A destination that names the same wallet as sourceWalletId returns 400 VALIDATION_ERROR; one that names the wallet the payout would otherwise pick as its source returns 422 INVALID_TRANSFER_PAIR. The optional remittance carries a reference and a description of your own. Both halves of the transfer read it back on remittance: the withdrawal that sends the funds, and the deposit that credits them. A fiat payout debits a virtual account and pays a bank recipient — set virtualAccountId and a destination.type: "fiat" with a rail and a recipient. The rail must settle the virtual account’s currency: fedwire, rtp, fednow and ach settle USD only, sepa settles EUR only, faster_payments and chaps settle GBP only, and swift carries any currency the correspondent chain supports. A rail that can’t settle the payout’s currency returns 422 PAYOUT_RAIL_CURRENCY_MISMATCH. The rail also decides which coordinates the recipient carries, so send only those. The POST /v2/payouts reference lists every rail the request accepts and the coordinates each recipient shape needs: a US recipient by routing and account number, a sepa one by IBAN, a uk_domestic one by sort code and account number. The same IBAN account is payable over sepa and over swift — the rail you name is the one it goes out on. The dollar example below applies unchanged to a euro or sterling account — the currency comes from the virtual account, not the request.
To move funds between two Conduit virtual accounts, send a virtual_account destination instead of bank coordinates. Both accounts must hold the same currency and belong to the same organization; the destination can be any customer of that organization, and it must differ from the source.
This arm carries no rail and no recipient. You name the destination account and Conduit routes the movement, so there are no bank coordinates to supply and no payment rail to choose. purpose is still required, and documents still applies, exactly as on the other arms. remittance is optional and is carried on both halves of the transfer. One refusal is specific to this arm: a destination holding a different currency returns 422 PAYOUT_DESTINATION_CURRENCY_MISMATCH, because a payout moves one currency and never converts. Use POST /v2/orders to convert between two virtual accounts of the same customer. The payout reads back with a virtual_account destination. How the money reaches the other account is Conduit’s to decide, so you do not choose a rail and do not need to know which bank holds either side. When the transfer settles, the receiving customer sees a matching deposit whose source is internal_transfer, carrying this payout’s id as originatingTransactionId, and both halves carry the remittance you sent. A virtual_account payout can also fail after it is accepted: if the two accounts cannot be connected for this transfer, the payout reaches status: "failed" and the reserved balance is released back to the source account — no deposit is created on the receiving side. Track the payout’s status (or subscribe to transaction.* webhooks) rather than assuming the deposit lands once the payout is accepted. Every request field, its type, and whether it is required are documented on the 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 prefunding, which never requires one. A payout that needs a document and attaches none is either refused or accepted and held until you send one, set per organization (see Payout requirements). For a crypto payout, the destination address only has to be well-formed for its chain, and it must pass the chain’s own checksum where it has one: a mixed-case EVM address must carry a correct EIP-55 checksum, and a Stellar G... address must pass its StrKey checksum. A mistyped address returns 400 INVALID_ADDRESS_FORMAT. Any valid address is accepted as a destination.

Destination memo

Some chains carry a memo (also called a destination tag) alongside the payment. Exchanges and other custodians that hold many end users behind a single address use it to credit the right account. When your recipient gives you an address and a memo, send both — a payment that arrives at a shared address without its memo is credited to nobody, and recovering it depends on the custodian. Set it on destination.memo, in whichever of the two forms the recipient specifies:
memo is optional and only accepted on chains whose payments carry one. Sending it on a chain that does not — any EVM chain, for example — returns 400 VALIDATION_ERROR rather than dropping the value silently.

Stellar destinations

A Stellar account has to be set up before it can receive USDC, and Conduit checks that when you create the payout rather than failing after the funds are reserved: Both are safe to retry unchanged once the recipient has acted — no transaction is created and no balance is reserved.

Payout requirements

purpose selects the requirement the payout must satisfy before it is accepted:
  • intercompany — on a payout that names a recipient, the recipient must be whitelisted for this customer, otherwise 422 RECIPIENT_NOT_WHITELISTED. Register a bank recipient via whitelist recipients (it must reach registered). For a crypto destination the register depends on who owns the wallet: your customer’s own wallet goes in registered addresses as a self_custody entry, and the payout sends attestation.custody: "self"; another entity in your customer’s corporate group goes in whitelist recipients on the crypto rail with relationship: group_entity (it must reach registered), and the payout sends attestation.custody: "third_party" with the third-party identity fields. A virtual_account destination names no recipient, so this whitelist rule does not apply to it. Supporting documents are not required for this purpose.
  • prefunding — reserved for funding your own customers’ Conduit-issued accounts from a designated house account; on a payout with bank-account or wallet-address coordinates, the destination must resolve to one of your customers’ active issued accounts, otherwise a 422 PREFUNDING_* error. A virtual_account destination names the account by id instead, and its refusals are 404 VIRTUAL_ACCOUNT_NOT_FOUND, 422 PAYOUT_DESTINATION_RESTRICTED, or 422 PAYOUT_DESTINATION_CURRENCY_MISMATCH — never a PREFUNDING_* error. Supporting documents are never required for this purpose.
  • Any other purpose — at least one supporting document is required. Upload each document with POST /v2/documents using purpose=transaction_support, then pass its doc_* id in documents. Document ids that don’t belong to your organization, or weren’t uploaded with purpose=transaction_support, return 400 DOCUMENT_IDS_NOT_FOUND. After acceptance the payout is held while the documents are reviewed; if the review is declined the payout ends as failed and a transaction.rejected webhook fires with reasonCategory: "document_inadequate".

A payout that needs a document and attaches none

What the create call does here is set per organization. It either refuses the request with 422 DOCUMENTATION_REQUIRED (the response lists acceptedDocumentTypes) — attach the document and retry — or it returns 202, accepts the payout, holds it, and asks you for the document:
  1. POST /v2/payouts returns the payout as usual. No money leaves the customer’s balance, but the amount is reserved while the payout waits, so it is not available for another payout.
  2. An rfi.published webhook fires and the request for information is delivered to your organization’s users by email. GET /v2/rfis/{id} carries the ask, the document types that satisfy it, and the due date.
  3. GET /v2/payouts/:id reports stage: "awaiting_customer_action" with hasRfi: true and rfiId naming that request, for as long as the payout waits.
  4. Answer the request with a supporting document. An approved answer releases the payout and it continues like any other. A declined answer, or a due date that passes with no answer, ends the payout as failed with failureCode: "compliance_rejected", and a transaction.rejected webhook fires with reasonCategory: "document_inadequate".
Attach the document on the create call and neither applies. A new organization collects after acceptance. An organization configured to collect before acceptance keeps the 422 until Conduit changes its arrangement. An organization with no portal user always gets the 422 either way, because nobody can receive the request. The submitted documents are not echoed back on GET /v2/payouts/:id, and a payout held for review of documents you did attach reads as a normal pending with stage: "under_review" (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 for POST /v2/payouts and GET /v2/payouts/:id.
Every response field is documented on the 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

Cancel a payout before it broadcasts on-chain. The reserved balance is released back to the customer’s available balance and a transaction.cancelled webhook fires with cancellationReason: "client_cancelled".
Headers: 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 — the response you got the first time replays for 30 days. 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 payouts move through pending quickly and hand off to the rail 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 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 is non-custodial, the payout requires the customer’s passkey approval before broadcast. Conduit cannot move a customer’s non-custodial funds without their co-signature.

How it works

  1. Submit POST /v2/payouts as usual. If requiresUserSignature: true appears in the response, the payout is awaiting your customer’s signature.
  2. The payout is screened for compliance and Travel Rule first. Once it clears, transaction.processing fires (its status becomes processing), then the transaction.awaiting_signature webhook 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 carries verificationUrl + expiresAt on the payload itself.
  3. Redirect the customer to the webhook’s verificationUrl before expiresAt. Use the expiresAt timestamp from the payload as the deadline — it can be up to 24 hours out; do not assume a fixed 15-minute window.
  4. The customer approves with their passkey. Conduit broadcasts. transaction.completed fires 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:
The 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

If a signing window expires but rebuild attempts remain, a fresh link is issued:
See the Non-Custodial Wallets concept page for end-to-end flow detail, branding customization, and sandbox testing.

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

When transaction.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.