Overview
A payout is a client-initiated outbound transfer of funds to an external destination. UsePOST /v2/payouts to initiate and GET /v2/payouts/:id to track.
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:
| Field | Type | Description |
|---|---|---|
customerId | string | Required. ID of the customer initiating the payout. |
purpose | string | Required. Declared business purpose: INTERCOMPANY, TREASURY_MANAGEMENT, PAYMENT_FOR_GOODS_OR_SERVICES, PAYROLL, INVESTMENTS, or OTHER. Determines which compliance gate the payout must clear — see Payout requirements. |
assetAmount | object | Required. code, chain (for on-chain assets), and decimal amount. |
destination | object | Required. type: "crypto" with a recipient (rail: "CRYPTO", chain, address, attestation), or type: "fiat" with a rail and a bank recipient. |
virtualAccountId | string | Required for fiat payouts. The USD virtual account to debit. |
documents | string[] | Up to 10 supporting document IDs (doc_*), each uploaded with purpose=transaction_support. Required unless purpose is INTERCOMPANY — see Payout requirements. |
clientReferenceId | string | Optional client-supplied reference. Passed through on transaction.* webhooks. Omit if not needed. |
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.- 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.
| Field | Type | Description |
|---|---|---|
id | string | Payout (transaction) ID. Use this for GET /v2/payouts/:id and to match transaction.* webhooks. |
type | string | Always "withdrawal" for payouts. |
status | string | Current status: pending, processing, completed, failed, cancelled. |
clientReferenceId | string | Client-supplied reference from the request. Present only when supplied on create. |
purpose | string | The declared business purpose supplied on create. |
createdAt | string | ISO 8601 UTC timestamp. |
completedAt | string | ISO 8601 UTC timestamp when the payout completed. Omitted on non-completed payouts. |
failureCode | string | Present when status is failed and the cause is actionable. Omitted on non-failed payouts (including cancelled). See Failures. |
cancelledAt | string | ISO 8601 UTC timestamp when the payout was cancelled. Present only on status: cancelled. |
cancellationReason | string | Machine-readable cancellation reason. client_cancelled when the client called POST /v2/payouts/:id/cancel; expired is reserved for future lock-expiry paths. Present only on status: cancelled. |
Cancel a payout
transaction.cancelled webhook fires with cancellationReason: "client_cancelled".
| Header | Required | Description |
|---|---|---|
x-api-key | Yes | API key. |
idempotency-key | Yes | UUID. Cancel is a money-adjacent operation; the header dedupes accidental retries. |
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:
| Payout state | Behaviour |
|---|---|
pending (pre-broadcast, including non-custodial payouts awaiting signature) | Cancels. Reserved balance is released. status becomes cancelled with cancellationReason: "client_cancelled". |
processing (broadcast already initiated) | 409 PAYOUT_NOT_CANCELLABLE. The chain transfer cannot be unwound. |
completed | 409 PAYOUT_NOT_CANCELLABLE. |
cancelled (from a prior cancel of this same payout) | 200 — same cancelled shape as the first cancel. |
failed | 409 PAYOUT_NOT_CANCELLABLE. A genuinely failed payout cannot be cancelled. |
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:
| Status | Code | Reason |
|---|---|---|
400 | IDEMPOTENCY_KEY_REQUIRED / IDEMPOTENCY_KEY_INVALID | Header missing or malformed. |
404 | PAYOUT_NOT_FOUND | No payout exists with this id for your organization. |
409 | PAYOUT_NOT_CANCELLABLE | The payout’s broadcast has begun, or it has reached a non-cancellable terminal state. |
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. - Wait for the
transaction.awaiting_user_signaturewebhook. It fires after compliance and Travel Rule data exchange have cleared, and carriesverificationUrl+expiresAton the webhook 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.
POST /v2/payouts — response (non-custodial)
The requiresUserSignature field is available immediately on the POST response: it’s true while the payout is awaiting the customer’s signature, and flips to false once the payout reaches a terminal status (completed or failed). The verify URL + expiry are not part of the response shape — they arrive on the transaction.awaiting_user_signature webhook once compliance and Travel Rule data exchange have cleared. Subscribe to that webhook to receive verificationUrl and expiresAt.
| Field | Type | Description |
|---|---|---|
requiresUserSignature | boolean | Present-tense gate: true while the payout is awaiting the end-user’s signature, false after status reaches completed or failed (regardless of the source wallet’s custody model). Fully-custodial wallets always emit false. Available immediately on the POST response — use this to prepare your UX without waiting for the webhook. |
queuePosition | number | Schema-reserved field describing the payout’s slot in the per-(wallet, chain) signing queue (0 = head of queue / signing now). Currently always omitted from the response; the queue counter that populates it is a follow-up. Treat as absent today and forward-compatible. |
GET /v2/payouts/:id — response while awaiting signature
Once compliance and Travel Rule data exchange have cleared, the signing step opens. The verify URL + expiry are delivered on the transaction.awaiting_user_signature webhook, not on the GET response. The GET response continues to report requiresUserSignature: true while the payout is parked awaiting the user’s cosign:
verificationUrl + expiresAt arrive on the transaction.awaiting_user_signature webhook payload. expiresAt is an ISO 8601 UTC timestamp; after it elapses the payout fails automatically with failureCode: "USER_SIGNATURE_TIMEOUT" 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.
| Code | What happened | What to do |
|---|---|---|
USER_SIGNATURE_TIMEOUT | The customer did not approve the payout before the signing window expired. | 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. |
TRAVEL_RULE_REJECTED | The counterparty VASP rejected the Travel Rule transfer before the on-chain broadcast. | No funds were moved. Confirm beneficiary details with the recipient and submit a new payout once the underlying issue has been addressed. |
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.