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
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. 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.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 → it is cancelled:
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(source short at execution),provider_unavailable(transient — retry may succeed),provider_rejected(declined — change funding source or recipient),internal_error(contact support), orcancelled.
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.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.