simulate/* endpoints. Crypto is not mocked — the deployed sandbox runs a real public testnet, so an offramp broadcasts its crypto source leg on a real testnet. See Real testnet broadcast below, or the full walkthrough in Use a real testnet.
This page walks the full OFFRAMP lifecycle — crypto-in on a customer wallet, FX conversion, fiat-out payout — end to end. The conversion legs are internal movements that settle automatically; the external-actor steps you drive manually are the customer’s inbound crypto deposit and, for a non-custodial wallet, the signature that releases the source crypto leg. Because the conversion legs are internal and have no real external actor, there are no per-leg failure injection endpoints for them — use orders/:id/simulate/conversion-failed to force the whole order to a failed terminal state, or arm destination payout failures at create-time via the autoPayout.recipient.bankName magic values or the accountNumber suffix protocol described below.
Prerequisites
- An
ACTIVEcustomer with a sandbox API key. If you haven’t onboarded one yet, run Sandbox quickstart first. - A crypto wallet for the customer holding the source asset (e.g. USDC on Ethereum), funded from a faucet on the deployed real-testnet sandbox — see Use a real testnet.
- Base URL:
https://api.sandbox.conduit.financial. Export your key:
Lifecycle overview
An OFFRAMP order moves crypto from a customer wallet to a fiat destination. In sandbox the path is:- Fund and clear the source crypto — the customer’s wallet must hold real testnet crypto. Fund it from a faucet and let the inbound deposit clear on-chain finality and the sender-information gate before you offramp (see Use a real testnet). For a deposit-funded order, send real testnet funds from a registered address to the Conduit-provided funding address. The transfer is an ordinary
deposittransaction you can read back. - The source crypto leg broadcasts — for a non-custodial wallet, the leg parks at
transaction.awaiting_signatureuntil you approve it with a real passkey or a machine stamp atPOST /v2/signing-requests/:id/approve; a custodial wallet signs nothing and broadcasts immediately. Either way, the broadcast then waits for on-chain finality — minutes, not seconds — and carries a realtxHashyou can verify on the testnet’s explorer. - The conversion and fiat payout settle — the FX conversion leg and the fiat payout are mocked and settle deterministically once the source leg is final; no manual settle calls are required.
order.succeededfires — after the source leg reaches finality and the conversion completes. On the deployed sandbox, wait for real chain finality.
order.created fires as soon as the order is accepted, and the order reaches succeeded or failed as the legs settle — both are webhooks. The source crypto leg is itself a transaction (type: "offramp"): it fires transaction.created, transaction.processing, transaction.awaiting_signature (non-custodial wallets only), and transaction.completed as it moves, each carrying linkedOrderId back to the order. See Webhook events below for the full set.
Real testnet broadcast
The source crypto leg of an offramp broadcasts on a real testnet — for every wallet provisioned with real testnet custody, with no per-request opt-in. A real broadcast spends real testnet funds, so fund the source wallet from a faucet first. For the supported testnets, how to fund a wallet from a faucet, and how to verify the transfer on a block explorer, see Use a real testnet.Full happy path
Step A — Create the OFFRAMP order
autoPayout.purpose is required. Every purpose except intercompany,
personal_transfer_of_funds and, by
default, prefunding (documentation policy can still require documents on a
prefunding payout above a configured amount) also requires at least one
previously-uploaded supporting document: upload it first with POST /v2/documents (purpose transaction_support) and pass its doc_... id in
autoPayout.documents. Omit it and what happens is set per organization: the
order is refused with 422 DOCUMENTATION_REQUIRED, or the order is created
and the autoPayout leg holds for a request for information (see
DOCUMENTATION_REQUIRED). The examples below
use payment_for_goods_or_services, so add your own documents id before
running them — or use "purpose": "intercompany" with a recipient already on
the customer’s whitelist (no document needed).source is optional. This walkthrough names the customer’s wallet as the
source because the wallet already holds the crypto. To convert crypto that
hasn’t arrived yet, omit source and send sourceAsset instead — see
Deposit-funded OFFRAMP below.202 Accepted. Capture id as ORDER_ID. The order is in pending, and order.created fires immediately — see Webhook events for every event the order and its source leg emit as they progress.
On a US rail (fedwire, rtp, fednow, ach, ach_same_day), autoPayout.recipient.bankAddress is optional. Omit it and Conduit derives the bank name and address from routingNumber when it creates the order, and the order keeps them. The sandbox directory holds every routing number except 999999992. An unheld routing number returns 400 VALIDATION_ERROR with pointer /autoPayout/recipient/bankAddress. When the directory cannot answer, the request returns 503 RECIPIENT_BANK_LOOKUP_UNAVAILABLE, which you can retry.
Step B — Fund the source wallet
The customer’s inbound crypto deposit and, for a non-custodial wallet, the signature that releases the source crypto leg are the external-actor steps in the OFFRAMP flow. If the wallet from the Prerequisites isn’t funded yet, send real testnet crypto to it now —autoExecute starts the order as soon as the balance covers the total debit, whether that happens before or after you create the order.
A faucet sends from an address the customer hasn’t registered, so the deposit parks at awaiting_sender_information — clear it with POST /v2/transactions/:id/sender-information and {"register": true} (registering the faucet address lets later faucet deposits skip the gate). Once it clears and reaches finality, watch for the order’s transaction.* webhooks and order.succeeded. See Use a real testnet.
Deposit-funded OFFRAMP (no source)
The walkthrough above starts from a wallet that already holds the crypto. The other shape starts from nothing: omitsource, let Conduit hand you an address, and fund that address. Read Deposit-Funded Orders for the full contract — this section is the sandbox drive.
Step 0 — Register the sending address
A funding address accepts money only from an address the customer registered, so do this before anything else:201 Created — screening clears immediately in sandbox. Skip this step and the funds you send in Step 2 are returned, not credited: the deposit transaction gets transaction.failed, a deposit_return transaction records the return, and the order reaches order.cancelled once its funding deadline elapses with no funds received.
Step 1 — Create the order with no source
Send sourceAsset in place of source, and omit autoExecute entirely (sending it is a 400):
202 Accepted. The response has no source key and carries depositInstructions instead:
Step 2 — Fund the address
Send real testnet crypto from the registered sender address to thedepositInstructions address. Send the order’s totalDebit, including fees, before lockExpiresAt. The transfer is an ordinary deposit transaction, readable through GET /v2/transactions.
Step 3 — Watch it auto-execute
After chain finality, the funds clear and the order executes itself.order.succeeded fires, followed by the chained payout Withdrawal exactly as in the happy path above. Along the way the funding transfer fires the standard transaction.created / .completed on its own deposit transaction, and — for anything not consumed — a deposit_return transaction naming it via returnOf.
Held funding transfers
A transfer held for compliance review can be resolved throughPOST /v2/sandbox/orders/${ORDER_ID}/deposits/simulate/compliance-decision with approve or reject. The order remains pending while the transfer is held. See Deposits.
Compliance failure on the destination payout
SetautoPayout.recipient.bankName to one of the magic values below at order-creation time. The conversion completes normally — the OFFRAMP order reaches succeeded. The compliance check fires on the chained Withdrawal transaction that the order spawns to deliver the fiat payout, and that chained transaction is held for review at status: "pending" rather than failing on its own. Integrators have to listen for both halves: order.succeeded on the order, then transaction.failed on the chained payout once the review is rejected.
bankName and fund the source crypto leg (Step B above — a faucet deposit, plus a signature if the wallet is non-custodial). The conversion auto-finalizes (order.succeeded) and the OFFRAMP order spawns its chained payout Withdrawal (transaction.created carries linkedOrderId pointing back at the parent order id). The chained Withdrawal is then held for compliance review — it keeps status: "pending" and does not fail on its own.
Terminalize it with POST /v2/sandbox/transactions/:id/simulate/compliance-decision and body { "outcome": "reject" }, passing the chained Withdrawal’s id. The Withdrawal then fires transaction.failed with compliance_review_rejected. Once a payout is held for a rejected review, { "outcome": "approve" } is not available — a rejected case can only be terminalized via reject.
The value is case-sensitive. Any other
bankName takes the happy path.
Destination payout rail failure
To test a fiat rail rejection on the chained payout (after a successful conversion), setautoPayout.recipient.accountNumber to a value ending in the following suffixes at order-create time. The conversion completes normally. For 94009001, 94009002 and 94009003, the chained payout then fails with the corresponding failureCode on a separate transaction.failed webhook for the Withdrawal transaction. For 94009004, transaction.created already fired when the Withdrawal was dispatched; no terminal webhook (transaction.completed or transaction.failed) fires until you settle the payout.
Create the order with the magic
accountNumber, fund the source crypto leg (Step B — a faucet deposit, plus a signature if the wallet is non-custodial), and observe the OFFRAMP order complete. For 94009001 to 94009003, a transaction.failed follows on the chained Withdrawal. 94009004 stays in flight until simulate/settled and fails only on a failed outcome. No mid-flow simulate call is needed, except simulate/settled for 94009004.
Rate lock expiry
To test what happens when the rate lock window expires before the order is executed:200 OK returning the order at its current state. Expires the order’s rate lock now. Wait for the order.cancelled webhook with reason: "expired". Semantics are identical to the ONRAMP variant; see ONRAMP orders for a worked example.
The order reaches
cancelled (expired) within seconds. There is no fixed
delay. Wait for the order.cancelled webhook.Conversion failure
To force the entire order to a failed terminal state, callorders/:id/simulate/conversion-failed at any point after the order is created. The endpoint is timing-independent: if the conversion has not yet started, the failure is armed and applied as soon as it does; if it is already in flight, it is aborted regardless of which leg the order is on:
200 OK returning the order at its current state. Webhook: order.failed carrying orderId, customerId, clientReferenceId, reasonCode, failureMessage (the standard text for that reasonCode, as in production), and failedAt. See Conversions for the full endpoint reference.
Order lifecycle states
The order’s
status only ever shows pending, succeeded, failed, or
cancelled — there’s no separate order status for “source settled” or
“conversion in progress”. But the order’s own offramp transaction (the
source leg) does emit intermediate webhooks as it moves: see
transaction.created / .processing / .awaiting_signature / .completed
below.Webhook events
The forward direction is also available:
GET /v2/orders/:id returns
linkedTransactionIds: string[] — every transaction the order has spawned so
far. The array starts empty and grows as the order progresses; it stays in
sync across /cancel, /execute, and subsequent GETs. Use it when you have
an order id and need to fan out to every transaction it produced without
scanning a webhook log.Errors
See Errors for the full error shape. On live, Conduit refuses an offramp for a customer in the United States. Sandbox accepts it. See Customers in the United States. When the OFFRAMP order itself fails — for example a conversion aborted viaorders/:id/simulate/conversion-failed — it surfaces on order.failed. Branch on the exact lowercase reasonCode wire value:
reasonCode is one of insufficient_funds, provider_unavailable, provider_rejected, internal_error, cancelled, insufficient_onchain_balance or source_account_unavailable. See the webhooks reference for the full order.failed schema, including the optional failureMessage.
failureCode values your integration should branch on when the OFFRAMP destination payout fails — these surface on the chained Withdrawal’s transaction.failed event, not on the OFFRAMP order, and are lowercase too:
compliance_review_rejected— the payout recipient failed the compliance check; not retryable without investigation.rail_policy_rejected/insufficient_funds_at_settle/rail_unavailable— payment-rail failures; adjust amount, recipient, or rail and retry.
Sequence diagram
Related pages
ONRAMP orders
Fiat-in to crypto-out — the mirror flow
Deposits
Fund a wallet from a testnet faucet
Conversions
FX conversion leg mechanics and failure scenarios
Deposit-Funded Orders
Orders created with no source, and the funds-returned caveats
Errors
Full error catalog and failureCode reference