Skip to main content
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, Virtual Accounts) and out to the endpoint reference (Payouts, Orders). It does not re-list them.
Test this flow in sandbox. Drive it end-to-end with simulated money and deterministic controls — start with the sandbox quickstart, then the sandbox overview for this flow, and the cheat sheet for every magic value and simulate endpoint.

Prerequisites

The journey at a glance

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

Step 1 — Fund the Virtual Account

1

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. A non-active account (pending_activation, disabled) cannot receive a usable deposit yet. The Virtual Accounts page covers that lifecycle.
2

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

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 pendingcompleted.
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.
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.
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.
  • A compliance review parks the deposit → transaction.failed with failureCode: compliance_hold. See COMPLIANCE_HOLD.
  • The sender address is unregistered → handled by the awaiting_sender_information path above; SENDER_INFO_TIMEOUT is the terminal failure if it expires.

Step 2 — The balance becomes available

1

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:
2

Advance: read the balance

Read the Virtual Account and confirm available reflects the deposit before you spend it.
Spend against available only. Initiating a payout or order for more than the available amount is rejected at submission. The Virtual Accounts page is the source of truth for the balance shape.

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

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

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

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 pendingsucceeded.
What can go wrong
  • No live rate for the pair at creation → POST /v2/orders returns RATE_UNAVAILABLE. See 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.
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.

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 reference documents the request body and every field, and the Send a payout guide walks the full lifecycle. This section is about the states it passes through.
1

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

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).
3

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). Multi-signer payouts also emit transaction.signature_collected per stamp and transaction.quorum_met once enough are in. The Non-Custodial Wallets page covers that branch end to end.
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.
  • A crypto destination address is malformed for its chain → 400 INVALID_ADDRESS_FORMAT at submission. See INVALID_ADDRESS_FORMAT.
  • A required supporting document is missing → 422 DOCUMENTATION_REQUIRED at submission. See 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, USER_SIGNATURE_DECLINED.

Step 5 — The payout reaches the rail

1

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

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.
What can go wrong at the rail

Step 6 — Terminal state

The transaction stops at exactly one terminal state. This is where your integration takes its final action.
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 reference.

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.