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.
Prerequisites
- An onboarded customer. See Onboard a Customer.
- A Virtual Account on that customer. See Add Virtual Accounts.
- A webhook endpoint subscribed to the
transaction.*andorder.*events. See Webhooks.
The journey at a glance
{ "code": "USD", "amount": "100.00" }). Echo what Conduit sends you; never round-trip through a float.
Step 1 — Fund the Virtual Account
State: the account is active, balance is zero
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.Advance: send funds to the deposit instructions
depositInstructions[] returned on the Virtual Account. No API call starts a deposit. Conduit detects the incoming funds and opens a deposit transaction for you.Webhooks: transaction.created, then transaction.completed
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.source.assetAmount is what the sender sent, and
destination.assetAmount is what was credited to the Virtual Account. The
difference is reported in fees[] — on GET /v2/transactions/{id} and on the
transaction.completed payload. 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
and its two amounts are equal.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.- The deposit is reversed or compliance returns it before credit →
transaction.failedwithfailureCode: returned_by_sender. See RETURNED_BY_SENDER. - A compliance review parks the deposit →
transaction.failedwithfailureCode: compliance_hold. See COMPLIANCE_HOLD. - The sender address is unregistered → handled by the
awaiting_sender_informationpath above; SENDER_INFO_TIMEOUT is the terminal failure if it expires.
Step 2 — The balance becomes available
State: funds are credited and spendable
transaction.completed fires for the deposit, the credited amount appears in the Virtual Account’s balances[].available. Each balance carries three buckets:Advance: read the balance
available reflects the deposit before you spend it.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.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.Advance: execute (or let it auto-execute)
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. 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); 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.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.- No live rate for the pair at creation →
POST /v2/ordersreturnsRATE_UNAVAILABLE. See RATE_UNAVAILABLE. - The lock expires before the order is executed → the expiry sweep cancels it:
order.cancelledwithreason: 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
sourceAssetyou asked to be funded in cannot fund an order this way →400 UNSUPPORTED_ASSET. - Execution fails terminally →
order.failedwith areasonCode: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), orcancelled. This is distinct from a manual/executecall returning422 INSUFFICIENT_FUNDSup front — that leaves the orderpendingand retryable, it is never a terminalorder.failed.
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 awithdrawal-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.
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.Advance: usually nothing — Conduit drives it
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).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.pending
- The recipient isn’t whitelisted for an
intercompanypayout →422 RECIPIENT_NOT_WHITELISTEDat submission. See RECIPIENT_NOT_WHITELISTED. - A crypto destination address is malformed for its chain →
400 INVALID_ADDRESS_FORMATat submission. See INVALID_ADDRESS_FORMAT. - A required supporting document is missing →
422 DOCUMENTATION_REQUIREDat submission. See DOCUMENTATION_REQUIRED. - The document review is declined after acceptance →
transaction.rejectedwithreasonCategory: document_inadequate(the payout readsfailed/failureCode: compliance_rejected). - The customer never signs or declines (non-custodial) →
transaction.failedwithuser_signature_timeoutoruser_signature_declined. See USER_SIGNATURE_TIMEOUT, USER_SIGNATURE_DECLINED.
Step 5 — The payout reaches the rail
State: processing
processing. The funds are
committed. The payout can no longer be cancelled.Advance: wait for finality
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.- The source is short at settlement →
transaction.failedwithinsufficient_funds_at_settle. See INSUFFICIENT_FUNDS_AT_SETTLE. - The chosen rail isn’t available →
rail_unavailable; a rail-policy rule blocks it →rail_policy_rejected. See RAIL_UNAVAILABLE, RAIL_POLICY_REJECTED. - A crypto broadcast is declined pre-broadcast →
provider_rejected. See PROVIDER_REJECTED. - The counterparty VASP rejects the Travel Rule transfer →
travel_rule_rejected. See TRAVEL_RULE_REJECTED.
Step 6 — Terminal state
The transaction stops at exactly one terminal state. This is where your integration takes its final action.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.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.Reacting to the terminal event
completed— store the settlement reference for reconciliation. The journey is done.failedwith an actionablefailureCode— follow the matching error playbook above; most resolve to “fix the input and submit a new payout with a freshidempotency-key.”failedwith nofailureCode— treat as terminal; contact support.cancelled— no funds moved; reserved balance is back inavailable. Submit a new payout when ready.