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

# Money Movement Lifecycle

> The journey of a dollar through Conduit: fund a Virtual Account, see the balance, convert it, pay it out, and follow the transaction to a terminal state

Money moves through Conduit in order. A customer funds a Virtual Account, the balance becomes available, you optionally convert it to another asset, you initiate a payout, and the payout's transaction walks through a few states until it reaches a terminal one.

Each step below answers the same four questions: **what state you're in**, **what you do to advance**, **what webhook fires**, and **what can go wrong**. Conduit is submit-and-listen — you make one call and react to webhooks. You can poll the matching `GET` endpoint, but you should never have to.

This guide is the map. It links down into the concept pages that own the authoritative field lists ([Money](/concepts/money), [Virtual Accounts](/concepts/virtual-accounts)) and out to the endpoint reference ([Payouts](/api-reference/payouts/create-a-payout), Orders). It does not re-list them.

<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 [the sandbox
  overview](/sandbox/overview) for this flow, and the [cheat
  sheet](/sandbox/cheat-sheet) for every magic value and simulate endpoint.
</Note>

## Prerequisites

* An onboarded customer. See [Onboard a Customer](/guides/onboard-customer).
* A Virtual Account on that customer. See [Add Virtual Accounts](/guides/add-virtual-accounts).
* A webhook endpoint subscribed to the `transaction.*` and `order.*` events. See [Webhooks](/webhooks).

## The journey at a glance

```
Fund the Virtual Account        → deposit transaction: pending → completed
        │
        ▼
Balance becomes available       → balances[].available increases
        │
        ▼ (optional)
Convert the balance             → order: pending → succeeded
        │
        ▼
Initiate a payout               → payout transaction: pending → processing → completed
        │
        ▼
Terminal state                  → completed · failed · cancelled
```

Amounts everywhere in this journey use the one [Money shape](/concepts/money) (`{ "code": "USD", "amount": "100.00" }`). Echo what Conduit sends you; never round-trip through a float.

## Step 1 — Fund the Virtual Account

<Steps>
  <Step title="State: the account is active, balance is zero">
    You start with an `active` Virtual Account. You reached it by submitting a `virtual_account` feature application and receiving `virtual_account.activated` — see [Add Virtual Accounts](/guides/add-virtual-accounts). A non-`active` account (`pending_activation`, `disabled`) cannot receive a usable deposit yet. The [Virtual Accounts](/concepts/virtual-accounts) page covers that lifecycle.
  </Step>

  <Step title="Advance: send funds to the deposit instructions">
    Your customer sends funds to the `depositInstructions[]` returned on the Virtual Account. No API call starts a deposit. Conduit detects the incoming funds and opens a deposit transaction for you.

    ```bash theme={null}
    curl https://api.conduit.financial/v2/customers/{customerId}/virtual-accounts/{virtualAccountId} \
      -H "x-api-key: YOUR_API_KEY"
    ```
  </Step>

  <Step title="Webhooks: transaction.created, then transaction.completed">
    Conduit fires `transaction.created` with `type: "deposit"` when it detects the inbound funds, then `transaction.completed` once the deposit settles and the balance is credited. The public transaction status moves `pending` → `completed`.
  </Step>
</Steps>

<Note>
  **A fiat deposit may be charged a fee.** When Conduit takes that fee out of
  the credit, the deposit's two side amounts differ: `source.assetAmount` is
  what the sender sent, and `destination.assetAmount` is what was credited to
  the Virtual Account. The fee is reported in `fees[]` — on
  `GET /v2/transactions/{id}` and on the `transaction.completed` payload. When
  Conduit invoices the fee to you separately instead, the two amounts are equal
  and the fee is still reported in `fees[]`. Post the destination amount to your
  own ledger and read the fee from `fees`; don't subtract it a second time.
  `fees` is always present on both — a deposit charged nothing carries an empty
  array. A conversion is different: a conversion fee Conduit invoices to you is
  on your monthly invoice only, and neither the order nor its transaction
  reports it in `fees`.
</Note>

<Note>
  On fiat deposits that arrived from an outside bank, the transaction's `source`
  is `external_bank_inbound` and carries `rail` (the network the deposit arrived
  on, such as `fedwire` or `ach`, or `null` when not known — read it rather than
  inferring it from the wire references) and a `sender` block — `name`,
  `country` (ISO 3166-1 alpha-2), `accountNumber`, `routingNumber`, `sortCode`,
  `iban`, `bic` — with what the sending bank transmitted; each field is present
  only when provided. A deposit delivered internally from another account in
  your own organization has no external sender: its `source` is
  `internal_transfer`, carrying `originatingTransactionId` (the payout that sent
  it) and no `sender` block — so branch on `source.type` rather than assuming
  every fiat deposit is `external_bank_inbound`. Amounts always live per-side
  (`source.assetAmount` / `destination.assetAmount`); transaction objects never
  carry a top-level `assetAmount` — only dedicated event payloads like
  `transaction.awaiting_sender_information` do.
</Note>

<Warning>
  A crypto deposit from an unregistered sender address parks at
  `transaction.awaiting_sender_information` instead of completing. The payload
  carries the `sourceAddress` and an `expiresAt` deadline. Register the sender
  before the deadline. After it, the deposit stays held, and Conduit compliance
  may return it to the sender.
</Warning>

**What can go wrong**

* The deposit is reversed or compliance returns it before credit → `transaction.failed` with `failureCode: returned_by_sender`. See [RETURNED\_BY\_SENDER](/errors/returned-by-sender).
* A compliance review parks the deposit → `transaction.failed` with `failureCode: compliance_hold`. See [COMPLIANCE\_HOLD](/errors/compliance-hold).
* The sender address is unregistered → handled by the `awaiting_sender_information` path above. After the deadline the deposit stays held; an older deposit failed with [SENDER\_INFO\_TIMEOUT](/errors/sender-info-timeout).

## Step 2 — The balance becomes available

<Steps>
  <Step title="State: funds are credited and spendable">
    Once `transaction.completed` fires for the deposit, the credited amount appears in the Virtual Account's `balances[].available`. Each balance carries three buckets:

    | Bucket | Meaning |
    | - | - |
    | `available` | Spendable now — this is what a payout or conversion can draw on. |
    | `pending` | Credited but not yet final; not spendable. |
    | `frozen` | Held by a compliance or operational hold, which includes money on a bank account the bank has blocked; not spendable. |
  </Step>

  <Step title="Advance: read the balance">
    Read the Virtual Account and confirm `available` reflects the deposit before you spend it.

    ```bash theme={null}
    curl https://api.conduit.financial/v2/customers/{customerId}/virtual-accounts/{virtualAccountId} \
      -H "x-api-key: YOUR_API_KEY"
    ```
  </Step>
</Steps>

<Note>
  Spend against `available` only. Initiating a payout or order for more than the
  `available` amount is rejected at submission. The [Virtual
  Accounts](/concepts/virtual-accounts) page is the source of truth for the
  balance shape.
</Note>

<Warning>
  What lands in `available` is the deposit's `destination.assetAmount`, which is
  net of any deposit fee. If you size a wire to cover an order, send the order's
  `totalDebit` plus the fee — wiring exactly `totalDebit` leaves the balance
  short and the order cannot execute.
</Warning>

## Step 3 — Convert the balance (optional)

Skip this step to pay out in the same asset you hold. Convert when the balance and the payout currency differ — for example, USD in a Virtual Account that you want to send out as USDC. A conversion is an **Order**: a firm, rate-locked exchange.

<Steps>
  <Step title="State: pending, with a locked rate">
    `POST /v2/orders` creates the order in status `pending` and returns `202 Accepted`. The response carries `sourceAsset`, `destinationAsset` (each `{ code, chain?, amount }`), and a `lockExpiresAt` — the rate holds until then, 2 hours after creation on an order with a named `source`. First confirm the route is supported and learn any required recipient fields with `GET /v2/orders/requirements`.

    `destination` always identifies a managed resource (`{ "type": "virtual_account", "id": "vac_..." }` or `{ "type": "wallet", "id": "wlt_...", "asset": { "code": "USDC", "chain": "ethereum" } }`), and so does `source` when you name one. `lockSide` picks which side `amount` is denominated in, and the order direction (onramp vs offramp) is inferred from the asset types — you don't send a `type`.

    `source` is optional. This step assumes you already hold the balance, so it names one. To convert crypto that hasn't arrived yet, omit `source` and send `sourceAsset` (`{ "code": "USDC", "chain": "ethereum" }`) instead — Conduit returns an address to fund the order at in `depositInstructions`, and the order executes itself once the funds clear. Those orders omit `source` from every response and carry caveats worth reading first: [Deposit-Funded Orders](/concepts/deposit-funded-orders).

    ```bash theme={null}
    curl https://api.conduit.financial/v2/orders \
      -X POST \
      -H "x-api-key: YOUR_API_KEY" \
      -H "idempotency-key: $(uuidgen)" \
      -H "content-type: application/json" \
      -d '{
        "source": { "type": "virtual_account", "id": "vac_..." },
        "destination": { "type": "wallet", "id": "wlt_...", "asset": { "code": "USDC", "chain": "ethereum" } },
        "lockSide": "source",
        "amount": "10000.00",
        "autoExecute": true
      }'
    ```
  </Step>

  <Step title="Advance: execute (or let it auto-execute)">
    A `pending` order does not move funds until executed, and creating it never checked whether the source was funded — that check only happens at execution. With `autoExecute: true`, Conduit automatically starts execution as soon as the available source balance covers `totalDebit` (principal plus fees, which can exceed the locked `amount`), with no client action required. Orders on the same source resource execute oldest-first, so a still-underfunded older order can block a newer, already-funded one until it's funded, cancelled, or expires. An older order already past `lockExpiresAt` that the balance cannot cover stops blocking immediately, and is cancelled at the same time. Otherwise, call `POST /v2/orders/:id/execute` yourself: each call makes one immediate attempt, returning `422 INSUFFICIENT_FUNDS` (order stays `pending`, unclaimed) if the balance is still short — fund the source and call it again. Either path must claim execution before `lockExpiresAt` or the order expires (see below) — except on a deposit-funded order, where a transfer that reached the funding address before the deadline meets it and the order stays `pending` until that transfer is decided; a manual `/execute` call also still works on an order created with `autoExecute: true`, so whichever reaches it first wins. To back out before execution, call `POST /v2/orders/:id/cancel`. On an order sourced from a non-custodial wallet that call also works after execution starts, while the source transfer is still awaiting signature.
  </Step>

  <Step title="Webhooks: order.created → order.succeeded">
    `order.created` fires on creation. `order.succeeded` fires once the source amount is debited and the destination amount is credited and spendable. `order.succeeded` carries the spawned `transactionId` so you can reconcile against the underlying transaction. The order status moves `pending` → `succeeded`.
  </Step>
</Steps>

**What can go wrong**

* No live rate for the pair at creation → `POST /v2/orders` returns `RATE_UNAVAILABLE`. See [RATE\_UNAVAILABLE](/errors/rate-unavailable).
* The lock expires before the order is executed → the expiry sweep cancels it: `order.cancelled` with `reason: expired`. On a deposit-funded order this is the funding deadline passing with nothing having arrived. A transfer that did arrive in time meets the deadline, so the order instead stays `pending` while that transfer is checked, and this event fires only if it is decided without crediting. Funds sent to the address after the deadline go back to the sender with no event of their own.
* The `sourceAsset` you asked to be funded in cannot fund an order this way → `400 UNSUPPORTED_ASSET`.
* Execution fails terminally → `order.failed` with a `reasonCode`: `insufficient_funds` (a rarer terminal path than the 422 above — e.g. a source deposit that later reversed after execution had already been claimed), `provider_unavailable` (transient — retry may succeed), `provider_rejected` (declined — change funding source or recipient), `internal_error` (contact support), `cancelled`, `insufficient_onchain_balance`, or `source_account_unavailable`. This is distinct from a manual `/execute` call returning `422 INSUFFICIENT_FUNDS` up front — that leaves the order `pending` and retryable, it is never a terminal `order.failed`. Money a bank block holds shows in `frozen` and does not count: an order whose source has only that money gets the `422 INSUFFICIENT_FUNDS` up front, not `source_account_unavailable`.

<Note>
  Order-level failures surface on `order.failed` with a `reasonCode`. The
  transaction-level `failureCode` vocabulary in the next step is separate. Don't
  expect a conversion failure on `transaction.failed`.
</Note>

## Step 4 — Initiate a payout

Now send the available balance out to a destination address. A payout is a `withdrawal`-type transaction. The [`POST /v2/payouts`](/api-reference/payouts/create-a-payout) reference documents the request body and every field, and the [Send a payout](/guides/send-payout) guide walks the full lifecycle. This section is about the states it passes through.

<Steps>
  <Step title="State: pending">
    `POST /v2/payouts` returns `202 Accepted` with the transaction in status `pending`. While `pending`, the payout clears compliance and Travel Rule exchange, and — for non-custodial wallets — awaits the customer's signature. The reserved balance is held but not yet sent.

    ```bash theme={null}
    curl https://api.conduit.financial/v2/payouts \
      -X POST \
      -H "x-api-key: YOUR_API_KEY" \
      -H "idempotency-key: $(uuidgen)" \
      -H "content-type: application/json" \
      -d '{
        "customerId": "cus_...",
        "virtualAccountId": "vac_...",
        "purpose": "treasury_management",
        "assetAmount": { "code": "USD", "amount": "1000.00" },
        "destination": { "type": "fiat", "rail": "fedwire", "recipient": { "...": "..." } },
        "documents": ["doc_..."]
      }'
    ```
  </Step>

  <Step title="Advance: usually nothing — Conduit drives it">
    A fiat payout advances on its own; a crypto payout first awaits the customer's signature (below). You can cancel the payout with `POST /v2/payouts/:id/cancel` until it is dispatched for settlement: submitted to the bank, or its on-chain send started. After that, the cancel returns `409 PAYOUT_NOT_CANCELLABLE`. Read the payout's `cancellable` field to decide whether to offer a cancel. Track state with `GET /v2/payouts/:id` (or the equivalent `GET /v2/transactions/:id`).
  </Step>

  <Step title="Webhooks: transaction.created (and signing events, if non-custodial)">
    `transaction.created` fires with `type: "withdrawal"`. For a non-custodial wallet source, `transaction.awaiting_signature` then fires with the `verificationUrl` to route the customer to (that 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)). Multi-signer payouts also emit `transaction.signature_collected` per stamp and `transaction.quorum_met` once enough are in. The [Non-Custodial Wallets](/concepts/non-custodial-wallets) page covers that branch end to end. When the payout holds for a review, `transaction.under_review` fires once as its `stage` becomes `under_review`. The event carries no reason. The payout then resumes or ends on its own, and the next event tells you which.
  </Step>
</Steps>

**What can go wrong while `pending`**

* The recipient isn't whitelisted for an `intercompany` or `personal_transfer_of_funds` payout → `422 RECIPIENT_NOT_WHITELISTED` at submission. See [RECIPIENT\_NOT\_WHITELISTED](/errors/recipient-not-whitelisted).
* A crypto destination address is malformed for its chain → `400 INVALID_ADDRESS_FORMAT` at submission. See [INVALID\_ADDRESS\_FORMAT](/errors/invalid-address-format).
* A required supporting document is missing → set per organization: either `422 DOCUMENTATION_REQUIRED`, or the payout is accepted and holds for a request for information until you answer it — a due date that passes doesn't fail the payout. See [DOCUMENTATION\_REQUIRED](/errors/documentation-required).
* The document review is declined after acceptance → `transaction.rejected` with `reasonCategory: document_inadequate` (the payout reads `failed` / `failureCode: compliance_rejected`).
* The customer never signs or declines (non-custodial) → `transaction.failed` with `user_signature_timeout` or `user_signature_declined`. See [USER\_SIGNATURE\_TIMEOUT](/errors/user-signature-timeout), [USER\_SIGNATURE\_DECLINED](/errors/user-signature-declined).

A `withdrawal` can also start at the bank: a third party pulls money from your customer's bank account, such as an ACH debit. It arrives already `completed`, and its destination is `external_bank_debit`, which names the party that pulled the money in `originator`. Without a name from the bank, the destination is `external_unknown`.

## Step 5 — The payout reaches the rail

<Steps>
  <Step title="State: processing">
    Once compliance clears and any required signature is in, the payout hands
    off to the rail (bank or chain) and moves to `processing`. The funds are
    committed. The payout can no longer be cancelled.
  </Step>

  <Step title="Advance: wait for finality">
    Nothing to do here. The transaction stays `processing` until the rail
    confirms settlement or reports a failure. Some rails (e.g. wire transfers)
    legitimately sit here for days — the `stage` field on `GET /v2/payouts/:id`
    (and `GET /v2/transactions/:id`) tells you why: `awaiting_signature`,
    `under_review`, or `settling`. Poll if you aren't relying on webhooks.
  </Step>
</Steps>

**What can go wrong at the rail**

* The source is short at settlement → `transaction.failed` with `insufficient_funds_at_settle`. See [INSUFFICIENT\_FUNDS\_AT\_SETTLE](/errors/insufficient-funds-at-settle).
* The chosen rail isn't available → `rail_unavailable`; a rail-policy rule blocks it → `rail_policy_rejected`. See [RAIL\_UNAVAILABLE](/errors/rail-unavailable), [RAIL\_POLICY\_REJECTED](/errors/rail-policy-rejected).
* A crypto broadcast is declined pre-broadcast → `provider_rejected`. See [PROVIDER\_REJECTED](/errors/provider-rejected).
* The counterparty VASP rejects the Travel Rule transfer → `travel_rule_rejected`. See [TRAVEL\_RULE\_REJECTED](/errors/travel-rule-rejected).

## Step 6 — Terminal state

The transaction stops at exactly one terminal state. This is where your integration takes its final action.

| Terminal status | Webhook | Meaning |
| - | - | - |
| `completed` | `transaction.completed` | Funds reached the destination. The settlement reference (e.g. `fedwireImad`, `achTraceNumber`, `txHash`) is on the relevant `source`/`destination` side of the payload. |
| `failed` | `transaction.failed` (or `transaction.rejected` for a declined document review) | The payout could not complete. Reserved funds, if any, are released. Branch on `failureCode`; when it's absent the cause isn't actionable — contact support. |
| `cancelled` | `transaction.cancelled` | Intentionally terminated (you called cancel, or a lock expired). Not a failure — there is no `failureCode`. `cancellationReason` is `client_cancelled` or `expired`. |

<Note>
  The customer-facing transaction status collapses to five values — `pending`,
  `processing`, `completed`, `failed`, `cancelled`. Compliance outcomes that
  look distinct internally all surface as `failed` on the public API,
  differentiated by `failureCode`. The full failure-code catalog and its
  per-code recovery steps live in the [Errors](/errors) reference.
</Note>

<Note>
  **A `completed` transfer can still come back.** Rare, but real: the receiving
  bank can send a wire or ACH credit back days after it settled — for a deposit,
  the sender's institution reverses it; for a payout, the recipient's bank
  returns it. When that happens the transaction flips straight from `completed`
  to `failed` with `failureCode: "returned_by_sender"`. For a returned payout,
  Conduit may issue a **replacement payout**; the replacement carries `retryOf`,
  the id of the payout it replaces. See
  [RETURNED\_BY\_SENDER](/errors/returned-by-sender).
</Note>

## Reacting to the terminal event

* **`completed`** — store the settlement reference for reconciliation. The journey is done.
* **`failed` with an actionable `failureCode`** — follow the matching error playbook above; most resolve to "fix the input and submit a new payout with a fresh `idempotency-key`."
* **`failed` with no `failureCode`** — treat as terminal; contact support.
* **`cancelled`** — no funds moved; reserved balance is back in `available`. Submit a new payout when ready.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.