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

# 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>
  On fiat deposits that arrived from an outside bank, the transaction's `source`
  is `external_bank_inbound` and carries a `sender` block — `name`, `country`
  (ISO 3166-1 alpha-2), `accountNumber`, `routingNumber`, `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, or the deposit times out and fails with `failureCode:
      sender_info_timeout`. See [SENDER\_INFO\_TIMEOUT](/errors/sender-info-timeout).
</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; [SENDER\_INFO\_TIMEOUT](/errors/sender-info-timeout) is the terminal failure if it expires.

## 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 action; 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>

## 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. 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. Execute it with `POST /v2/orders/:id/execute`, or let Conduit execute it when source funds land if you created it with `autoExecute: true`. To back out before execution, call `POST /v2/orders/:id/cancel`.
  </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 → it is cancelled: `order.cancelled` with `reason: expired`. On a deposit-funded order this is the funding deadline passing unfunded, and any funds sent to its address afterwards 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` (source short at execution), `provider_unavailable` (transient — retry may succeed), `provider_rejected` (declined — change funding source or recipient), `internal_error` (contact support), or `cancelled`.

<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 custodial payout advances on its own. You can still cancel a `pending` payout with `POST /v2/payouts/:id/cancel` until it broadcasts. Once it reaches `processing`, the cancel returns `409 PAYOUT_NOT_CANCELLABLE`. 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.
  </Step>
</Steps>

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

* The recipient isn't whitelisted for an `intercompany` 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 → `422 DOCUMENTATION_REQUIRED` at submission. 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).

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

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