Skip to main content
Fiat in sandbox is mocked: no real bank transfers, no third-party calls, and every fiat outcome is deterministic and driven by your request data or by sandbox-only 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 ACTIVE customer 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:
  1. 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 deposit transaction you can read back.
  2. The source crypto leg broadcasts — for a non-custodial wallet, the leg parks at transaction.awaiting_signature until you approve it with a real passkey or a machine stamp at POST /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 real txHash you can verify on the testnet’s explorer.
  3. 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.
  4. order.succeeded fires — 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: omit source, 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:
lockExpiresAt here is the funding deadline, not a rate expiry. depositInstructions[0].expiresAt equals it. Read it from the response and fund the order before it. Do not assume a fixed window. An order left sitting between steps is already cancelled (expired).

Step 2 — Fund the address

Send real testnet crypto from the registered sender address to the depositInstructions 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 through POST /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

Set autoPayout.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.
Create the order with the magic 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), set autoPayout.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, call orders/: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 via orders/: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

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