simulate/* endpoints.
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 only external-actor step you drive manually is the customer’s inbound crypto deposit. 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). See Deposits for injecting a synthetic balance.
- 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 through these stages:- Crypto deposit detected — the customer’s inbound crypto is received on their wallet. In sandbox you inject this with the
/wallets/:walletId/deposits/simulateendpoint (the customer’s wallet sending in is a real external-actor step). On a deposit-funded order the funds land on a Conduit-provided address instead, and you send them with/orders/:orderId/deposits/simulate— from an address the customer registered. This endpoint answers with the order, but the transfer is still an ordinarydeposittransaction you can read back. - Conversion auto-finalizes — the source and destination conversion legs are internal movements. The mock provider finalizes them automatically; no manual settle calls are required.
order.succeededfires — the order reachessucceededwithin seconds of the deposit being detected.
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.
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,
otherwise the create is rejected with 422 DOCUMENTATION_REQUIRED before the
order exists. 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 — Inject a synthetic crypto deposit
The customer’s inbound crypto deposit is the only external-actor step in the OFFRAMP flow. Inject a synthetic deposit following the same pattern documented at Deposits:order.succeeded fires within seconds — no further simulate calls are needed.
Poll GET /v2/orders/$ORDER_ID to observe progress if needed.
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
The funding address has no client-visible wallet id, so the order id is the handle:200 OK with the order — the transfer itself is still an ordinary deposit transaction, readable on GET /v2/transactions. Send the order’s totalDebit (the amount being converted plus any source-asset fee), not sourceAsset.amount — the order executes only once the funds cover totalDebit. sourceAddress must be an address registered in Step 0; anything else is bounced back silently. The asset and chain must match the order’s sourceAsset; a mismatch returns 400 VALIDATION_ERROR. Calling it on an order that named a source returns 409 SANDBOX_ORDER_NOT_DEPOSIT_FUNDED — there is no address to fund.
Step 3 — Watch it auto-execute
Nothing else to call. The funds clear, the order executes itself, andorder.succeeded fires within seconds, 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.
Variations
Everything below falls out of the same route.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; that chained transaction fails with the matching failureCode. Integrators have to listen for both halves: order.succeeded on the order followed by transaction.failed on the chained payout.
bankName and inject the synthetic crypto deposit (Step B above). 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 compliance gate fires on the chained Withdrawal and it terminates as transaction.failed.
The value is case-sensitive. Any other
bankName takes the happy path.
† On withdrawals (including the chained payout that a successful OFFRAMP order spawns), both REJECTED and sanctions classifications surface the same public COMPLIANCE_REVIEW_REJECTED failure code. The underlying classification is recorded for audit.
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, inject the synthetic deposit (Step B), 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 reason you supplied), 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.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:
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.
orders/:id/simulate/conversion-failed), the failure surfaces on order.failed with reasonCode.
Sequence diagram
Related pages
ONRAMP orders
Fiat-in to crypto-out — the mirror flow
Deposits
Injecting synthetic crypto balances into a customer wallet
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