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 for a customer provisioned now. 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 the mock provider auto-finalizes; the external-actor steps you drive manually are the customer’s inbound crypto deposit and — on the real-testnet sandbox — 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). Fund it from a faucet on the deployed real-testnet sandbox. See Deposits for funding instructions.
- 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 and is signed — the source leg leaves the customer’s non-custodial wallet on a real testnet, so it parks at
transaction.awaiting_signature. Approve with a real passkey, or a machine stamp atPOST /v2/signing-requests/:id/approve— thesimulate/cosignlever returns409 SANDBOX_SIGNING_SIMULATION_UNAVAILABLEhere. 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.
pending after creation and reaches succeeded or failed as the legs settle. Intermediate state is observable only via GET /v2/orders/:id (poll). Terminal status surfaces via webhook.
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. A wallet provisioned before real testnet was enabled cannot offramp — it is refused with422 LEGACY_MOCK_WALLET_UNSUPPORTED; create a new customer.
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 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. No webhook fires at creation time — intermediate state is polled via GET /v2/orders/:id.
Step B — Fund the source wallet
The customer’s inbound crypto deposit and the signature that releases the source crypto leg are the external-actor steps in the OFFRAMP flow. Send real testnet funds to the source wallet address. After detection and finality, watch the order throughGET /v2/orders/$ORDER_ID or its webhooks. 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 bounced straight back with no webhook and no trace.
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. Poll GET /v2/orders/${ORDER_ID} if you want to watch status move pending → succeeded. 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 the signature). 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; the chained payout then fails with the corresponding failureCode on a separate transaction.failed webhook for the Withdrawal transaction.
Create the order with the magic
accountNumber, fund the source crypto leg (Step B — a faucet deposit plus the signature), and observe the OFFRAMP order complete followed by a transaction.failed on the chained Withdrawal. No mid-flow simulate call is needed.
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. The rate lock timestamp is backdated so the next sweep treats the order as expired. Webhook: order.cancelled with cancellationReason: "expired". Semantics are identical to the ONRAMP variant; see ONRAMP orders for a worked example.
The order reaches
cancelled (expired) within about 3 seconds via an
immediate background sweep tick. No polling backoff needed; no need to wait
for the scheduled 30-second sweep.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
Intermediate steps (source settled, conversion in progress) are not reflected
as order statuses and do not emit webhooks. Poll
GET /v2/orders/{orderId}
for current state; only the terminal outcomes surface via webhook.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. 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. 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