> ## Documentation Index
> Fetch the complete documentation index at: https://v2.docs.conduit.financial/llms.txt
> Use this file to discover all available pages before exploring further.

# Send a payout

> Initiate, sign, track, and cancel outbound crypto and fiat transfers

<Note>
  **Test this flow in sandbox.** Drive it end-to-end with simulated money and deterministic controls — start with the [sandbox quickstart](/sandbox/quickstart), then [withdrawal simulation](/sandbox/withdrawals) for this flow, and the [cheat sheet](/sandbox/cheat-sheet) for every magic value and simulate endpoint.
</Note>

## Overview

A payout is a client-initiated outbound transfer of funds out of a customer's Conduit balance. Use [`POST /v2/payouts`](/api-reference/payouts/create-a-payout) to initiate and [`GET /v2/payouts/:id`](/api-reference/payouts/retrieve-a-payout) 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 from `customerId` + the `assetAmount` code/chain):

```json theme={null}
{
  "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`:

```json theme={null}
{
  "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"]
}
```

Every request field, its type, and whether it is required are documented on the [`POST /v2/payouts`](/api-reference/payouts/create-a-payout) endpoint reference. Two fields carry integration meaning worth calling out here: `purpose` selects the compliance requirement the payout must satisfy (see [Payout requirements](#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, otherwise `422 RECIPIENT_NOT_WHITELISTED`. Register a bank recipient via [whitelist recipients](/concepts/whitelist-recipients) (it must reach `registered`), or a crypto address via [registered addresses](/concepts/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 a `422 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 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`.

```json theme={null}
{
  "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"
}
```

Every response field is documented on the [`POST /v2/payouts`](/api-reference/payouts/create-a-payout) and [`GET /v2/payouts/:id`](/api-reference/payouts/retrieve-a-payout) 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](#progress-the-stage-field)), `failureCode` (see [Failures](#failures)), and, for non-custodial payouts, `requiresUserSignature` and `queuePosition` (see [Non-Custodial Crypto Withdrawals](#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`.

| Stage                      | Meaning                                                                                                                             | Customer action needed?                                                                                                                                                |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `awaiting_signature`       | Waiting on the required transaction signature.                                                                                      | Yes — see `requiresUserSignature` and the `transaction.awaiting_signature` webhook.                                                                                    |
| `awaiting_customer_action` | Further input is needed from your customer.                                                                                         | Yes — details arrive on the matching event for your integration (e.g. `transaction.awaiting_sender_information`). This is **not** an RFI — `hasRfi` does not cover it. |
| `under_review`             | Compliance, document, or verification review of something already submitted is in progress.                                         | Usually no — check `hasRfi` and your document-request webhooks for whether a response is needed from you.                                                              |
| `settling`                 | Funds movement or settlement is in progress, or nothing is currently blocking the payout. No customer action is currently required. | No.                                                                                                                                                                    |
| *(omitted)*                | The payout has reached a terminal `status`.                                                                                         | Check `status`, `failureCode`, and `failureMessage`.                                                                                                                   |

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

```http theme={null}
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"`.

```bash theme={null}
curl -X POST {{api-host}}/v2/payouts/$PAYOUT_ID/cancel \
  -H "x-api-key: $API_KEY" \
  -H "idempotency-key: $(uuidgen)"
```

Headers:

| Header            | Required | Description                                                                        |
| ----------------- | -------- | ---------------------------------------------------------------------------------- |
| `x-api-key`       | Yes      | API key.                                                                           |
| `idempotency-key` | Yes      | UUID. 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 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.                                       |

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:

| 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

```
POST /v2/payouts
       │
       ▼
 Compliance + Travel Rule (transparent — no client action needed)
       │
       ▼
 transaction.awaiting_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. 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` (default 15 minutes).
4. The customer approves with their passkey. Conduit broadcasts. `transaction.completed` fires when the chain confirms.

<Note>
  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](/guides/machine-signer-stamping).
</Note>

### `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`.

```json theme={null}
{
  "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"
}
```

| Field                   | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ----------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `requiresUserSignature` | boolean | `true` for any non-terminal payout that will require the end-user's signature — this includes payouts that are still **waiting in the queue or collecting signatures**, so it can be `true` before a signing link exists. It is NOT a signal that a link is ready to act on. Flips to `false` once `status` reaches `completed` or `failed` (regardless of the source wallet's custody model); fully-custodial wallets always emit `false`. Watch the `transaction.awaiting_signature` webhook for the actionable `verificationUrl` (the link to present to the user, with its expiry) — this applies to a wallet in the passkey signing mode; a wallet in a programmatic signing mode instead carries a `signingRequestId` on that webhook (see [Machine-signer stamping](/guides/machine-signer-stamping)) — and read `queuePosition` for the payout's queue state. Available immediately on the POST response — use it to prepare your UX without waiting for the webhook. |
| `queuePosition`         | number  | The payout's slot in the per-(wallet, chain) signing queue. **Waiting payouts** (queued behind a payout already in the signing step) expose this as a positive integer. The **active payout** (currently being signed) has the field absent from the response. Populated eventually — it may not appear on the initial `POST` response; check `GET /v2/payouts/:id` to observe the current value.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

### `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:

```json theme={null}
{
  "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_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

```
transaction.created
  ↓
transaction.awaiting_signature   ← route customer to verificationUrl
  ↓ (after customer approves; compliance + Travel Rule run, then chain confirms)
transaction.completed
```

If a signing window expires but rebuild attempts remain, a fresh link is issued:

```
transaction.awaiting_signature  (attempt: 1)
  ↓ (window expires, attempts remaining)
transaction.awaiting_signature  (attempt: 2)
  ↓ (all attempts exhausted or customer declines)
transaction.failed
```

See the [Non-Custodial Wallets concept page](/concepts/non-custodial-wallets) 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](#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.

| Code                                  | What happened                                                                                                     | What to do                                                                                                                                |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `user_signature_timeout`              | The payout waited too long in the signing queue without reaching the signing step (queue max-residence timeout).  | No funds were moved. Submit a new payout when the customer is ready to sign.                                                              |
| `user_signature_expired`              | Every signing window was offered to the customer but quorum was not reached; all rebuild attempts were exhausted. | 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. |

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.
