Skip to main content

Overview

A payout is a client-initiated outbound transfer of funds to an external destination. Use POST /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 from customerId + the assetAmount code/chain):
{
  "customerId": "cus_2xKjF9mQb7vN4hL1pR3w8t",
  "purpose": "TREASURY_MANAGEMENT",
  "assetAmount": {
    "code": "USDC",
    "chain": "ethereum",
    "amount": "1000.000000"
  },
  "destination": {
    "type": "crypto",
    "recipient": {
      "rail": "CRYPTO",
      "chain": "ethereum",
      "address": "0xRecipientAddress",
      "attestation": { "custody": "self" }
    }
  },
  "documents": ["doc_2xKjF9mQb7vN4hL1pR3w8t"],
  "clientReferenceId": "client-payout-001"
}
A fiat payout debits a USD virtual account and pays a bank recipient — set virtualAccountId and a destination.type: "fiat" with a rail (FEDWIRE, RTP, FEDNOW, SWIFT) and a recipient:
{
  "customerId": "cus_2xKjF9mQb7vN4hL1pR3w8t",
  "virtualAccountId": "vac_2xKjF9mQb7vN4hL1pR3w8t",
  "purpose": "TREASURY_MANAGEMENT",
  "assetAmount": { "code": "USD", "amount": "1000.00" },
  "destination": {
    "type": "fiat",
    "rail": "FEDWIRE",
    "recipient": {
      "rail": "US", "type": "INDIVIDUAL", "firstName": "Alice", "lastName": "Example",
      "accountNumber": "1234567890", "routingNumber": "021000021", "accountType": "CHECKING",
      "phone": "+15551234567",
      "postalAddress": { "addressLine1": "1 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "USA" },
      "bankAddress": { "addressLine1": "100 Wall St", "city": "New York", "country": "USA" }
    }
  },
  "documents": ["doc_2xKjF9mQb7vN4hL1pR3w8t"]
}
FieldTypeDescription
customerIdstringRequired. ID of the customer initiating the payout.
purposestringRequired. 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.
assetAmountobjectRequired. code, chain (for on-chain assets), and decimal amount.
destinationobjectRequired. type: "crypto" with a recipient (rail: "CRYPTO", chain, address, attestation), or type: "fiat" with a rail and a bank recipient.
virtualAccountIdstringRequired for fiat payouts. The USD virtual account to debit.
documentsstring[]Up to 10 supporting document IDs (doc_*), each uploaded with purpose=transaction_support. Required unless purpose is INTERCOMPANY — see Payout requirements.
clientReferenceIdstringOptional 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, otherwise 422 RECIPIENT_NOT_WHITELISTED. Register a bank recipient via whitelist recipients (it must reach REGISTERED), 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 lists acceptedDocumentTypes). 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".
The submitted 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 for POST /v2/payouts and GET /v2/payouts/:id.
{
  "id": "txn_2xKjF9mQb7vN4hL1pR3w8t",
  "customerId": "cus_2xKjF9mQb7vN4hL1pR3w8t",
  "type": "withdrawal",
  "status": "pending",
  "clientReferenceId": "client-payout-001",
  "purpose": "TREASURY_MANAGEMENT",
  "source": {
    "type": "wallet",
    "walletId": "wlt_2xKjF9mQb7vN4hL1pR3w8t",
    "address": "0xCustomerWalletAddress",
    "assetAmount": {
      "code": "USDC",
      "chain": "ethereum",
      "amount": "1000.000000"
    }
  },
  "destination": {
    "type": "external_crypto",
    "address": "0xRecipientAddress",
    "assetAmount": {
      "code": "USDC",
      "chain": "ethereum",
      "amount": "1000.000000"
    }
  },
  "fees": [],
  "createdAt": "2026-05-14T18:40:00.000Z"
}
FieldTypeDescription
idstringPayout (transaction) ID. Use this for GET /v2/payouts/:id and to match transaction.* webhooks.
typestringAlways "withdrawal" for payouts.
statusstringCurrent status: pending, processing, completed, failed, cancelled.
clientReferenceIdstringClient-supplied reference from the request. Present only when supplied on create.
purposestringThe declared business purpose supplied on create.
createdAtstringISO 8601 UTC timestamp.
completedAtstringISO 8601 UTC timestamp when the payout completed. Omitted on non-completed payouts.
failureCodestringPresent when status is failed and the cause is actionable. Omitted on non-failed payouts (including cancelled). See Failures.
cancelledAtstringISO 8601 UTC timestamp when the payout was cancelled. Present only on status: cancelled.
cancellationReasonstringMachine-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

POST /v2/payouts/:id/cancel
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".
curl -X POST {{api-host}}/v2/payouts/$PAYOUT_ID/cancel \
  -H "x-api-key: $API_KEY" \
  -H "idempotency-key: $(uuidgen)"
Headers:
HeaderRequiredDescription
x-api-keyYesAPI key.
idempotency-keyYesUUID. Cancel is a money-adjacent operation; the header dedupes accidental retries.
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:
Payout stateBehaviour
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.
completed409 PAYOUT_NOT_CANCELLABLE.
cancelled (from a prior cancel of this same payout)200 — same cancelled shape as the first cancel.
failed409 PAYOUT_NOT_CANCELLABLE. A genuinely failed payout cannot be cancelled.
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:
StatusCodeReason
400IDEMPOTENCY_KEY_REQUIRED / IDEMPOTENCY_KEY_INVALIDHeader missing or malformed.
404PAYOUT_NOT_FOUNDNo payout exists with this id for your organization.
409PAYOUT_NOT_CANCELLABLEThe 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

POST /v2/payouts


 Compliance + Travel Rule (transparent — no client action needed)


 transaction.awaiting_user_signature webhook fired

       ├── data.verificationUrl  ← route customer here
       └── data.expiresAt        ← sign before this time


 Customer visits /verify/<token> → passkey approval


 Broadcast → await finality → transaction.completed
  1. Submit POST /v2/payouts as usual. If requiresUserSignature: true appears in the response, the payout is awaiting your customer’s signature.
  2. Wait for the transaction.awaiting_user_signature webhook. It fires after compliance and Travel Rule data exchange have cleared, and carries verificationUrl + expiresAt on the webhook payload itself.
  3. Redirect the customer to the webhook’s verificationUrl before expiresAt (default 15 minutes).
  4. The customer approves with their passkey. Conduit broadcasts. transaction.completed fires 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.
{
  "id": "txn_2xKjF9mQb7vN4hL1pR3w8t",
  "customerId": "cus_2xKjF9mQb7vN4hL1pR3w8t",
  "type": "withdrawal",
  "status": "pending",
  "clientReferenceId": "client-payout-001",
  "purpose": "TREASURY_MANAGEMENT",
  "source": {
    "type": "wallet",
    "walletId": "wlt_2xKjF9mQb7vN4hL1pR3w8t",
    "address": "0xCustomerWalletAddress",
    "assetAmount": {
      "code": "USDC",
      "chain": "ethereum",
      "amount": "1000.000000"
    }
  },
  "destination": {
    "type": "external_crypto",
    "address": "0xRecipientAddress",
    "assetAmount": {
      "code": "USDC",
      "chain": "ethereum",
      "amount": "1000.000000"
    }
  },
  "fees": [],
  "requiresUserSignature": true,
  "createdAt": "2026-05-14T18:40:00.000Z"
}
FieldTypeDescription
requiresUserSignaturebooleanPresent-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.
queuePositionnumberSchema-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:
{
  "id": "txn_2xKjF9mQb7vN4hL1pR3w8t",
  "customerId": "cus_2xKjF9mQb7vN4hL1pR3w8t",
  "type": "withdrawal",
  "status": "pending",
  "clientReferenceId": "client-payout-001",
  "purpose": "TREASURY_MANAGEMENT",
  "source": {
    "type": "wallet",
    "walletId": "wlt_2xKjF9mQb7vN4hL1pR3w8t",
    "address": "0xCustomerWalletAddress",
    "assetAmount": {
      "code": "USDC",
      "chain": "ethereum",
      "amount": "1000.000000"
    }
  },
  "destination": {
    "type": "external_crypto",
    "address": "0xRecipientAddress",
    "assetAmount": {
      "code": "USDC",
      "chain": "ethereum",
      "amount": "1000.000000"
    }
  },
  "fees": [],
  "requiresUserSignature": true,
  "createdAt": "2026-05-14T18:40:00.000Z"
}
The 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

transaction.created
  ↓ (after compliance + Travel Rule)
transaction.awaiting_user_signature   ← route customer to verificationUrl
  ↓ (after customer approves and chain confirms)
transaction.completed
If the customer does not act in time, or declines:
transaction.awaiting_user_signature

transaction.failed
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.
CodeWhat happenedWhat to do
USER_SIGNATURE_TIMEOUTThe 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_DECLINEDThe 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_PROVIDERThe 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_MISCONFIGUREDThe 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_REJECTEDThe 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.
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.