Skip to main content
The sandbox is fully mocked. No real chain activity, no real bank transfers, no third-party calls. Every outcome is deterministic and driven by your request data or by sandbox-only 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 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). See Deposits for injecting a synthetic balance.
  • Base URL: https://api.sandbox.conduit.financial. Export your key:
export SANDBOX_API_KEY="ck_sandbox_..."
export CUSTOMER_ID="cus_..."
export WALLET_ID="wlt_..."
export VA_ID="vac_..."

Lifecycle overview

An OFFRAMP order moves crypto from a customer wallet to a fiat destination through these stages:
  1. Crypto deposit detected — the customer’s inbound crypto is received on their wallet. In sandbox you inject this with the /wallets/:walletId/deposits/simulate endpoint (the customer’s wallet sending in is a real external-actor step).
  2. Conversion auto-finalizes — the source and destination conversion legs are internal movements. The mock provider finalizes them automatically; no manual settle calls are required.
  3. order.succeeded fires — the order reaches succeeded within seconds of the deposit being detected.
The order starts in 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

curl -X POST "https://api.sandbox.conduit.financial/v2/orders" \
  -H "x-api-key: ${SANDBOX_API_KEY}" \
  -H "idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "clientReferenceId": "cref-offramp-1",
    "source": {
      "type": "wallet",
      "id": "'"${WALLET_ID}"'",
      "asset": { "code": "USDC", "chain": "ETHEREUM" }
    },
    "destination": {
      "type": "virtual_account",
      "id": "'"${VA_ID}"'"
    },
    "autoPayout": {
      "rail": "FEDWIRE",
      "recipient": {
        "rail": "US",
        "type": "INDIVIDUAL",
        "firstName": "Jane",
        "lastName": "Doe",
        "dateOfBirth": "1990-01-15",
        "countryOfCitizenship": "USA",
        "accountNumber": "123456789",
        "routingNumber": "021000021",
        "accountType": "CHECKING",
        "bankName": "First National Bank",
        "bankAddress": {
          "addressLine1": "270 Park Ave",
          "city": "New York",
          "state": "NY",
          "postalCode": "10017",
          "country": "USA"
        },
        "phone": "+12125551234",
        "postalAddress": {
          "addressLine1": "1 Market St",
          "city": "San Francisco",
          "state": "CA",
          "postalCode": "94105",
          "country": "USA"
        }
      }
    },
    "lockSide": "source",
    "amount": "100.000000",
    "autoExecute": true
  }'
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.
export ORDER_ID="ord_..."

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:
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/customers/${CUSTOMER_ID}/wallets/${WALLET_ID}/deposits/simulate" \
  -H "x-api-key: ${SANDBOX_API_KEY}" \
  -H "idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "assetAmount": { "code": "USDC", "chain": "ETHEREUM", "amount": "100" }
  }'
Once the deposit is detected, the mock provider auto-finalizes the conversion legs. Webhook: order.succeeded fires within seconds — no further simulate calls are needed. Poll GET /v2/orders/$ORDER_ID to observe progress if needed.

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 AML 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.
curl -X POST "https://api.sandbox.conduit.financial/v2/orders" \
  -H "x-api-key: ${SANDBOX_API_KEY}" \
  -H "idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "clientReferenceId": "cref-offramp-aml",
    "source": {
      "type": "wallet",
      "id": "'"${WALLET_ID}"'",
      "asset": { "code": "USDC", "chain": "ETHEREUM" }
    },
    "destination": { "type": "virtual_account", "id": "'"${VA_ID}"'" },
    "autoPayout": {
      "rail": "FEDWIRE",
      "recipient": {
        "rail": "US",
        "type": "INDIVIDUAL",
        "firstName": "Jane",
        "lastName": "Doe",
        "dateOfBirth": "1990-01-15",
        "countryOfCitizenship": "USA",
        "accountNumber": "123456789",
        "routingNumber": "021000021",
        "accountType": "CHECKING",
        "bankName": "SANDBOX_AML_REJECTED",
        "bankAddress": {
          "addressLine1": "270 Park Ave",
          "city": "New York",
          "state": "NY",
          "postalCode": "10017",
          "country": "USA"
        },
        "phone": "+12125551234",
        "postalAddress": {
          "addressLine1": "1 Market St",
          "city": "San Francisco",
          "state": "CA",
          "postalCode": "94105",
          "country": "USA"
        }
      }
    },
    "lockSide": "source",
    "amount": "100.000000",
    "autoExecute": true
  }'
Create the order with the magic 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.
bankName magic valuefailureCode on the chained Withdrawal transaction.failed
SANDBOX_AML_REJECTEDAML_REJECTED
SANDBOX_AML_SANCTIONEDAML_REJECTED
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 AML_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), set autoPayout.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.
accountNumber last 8 digitsfailureCode on the Withdrawal
94009001RAIL_POLICY_REJECTED
94009002INSUFFICIENT_FUNDS_AT_SETTLE
94009003RAIL_UNAVAILABLE
94009004RAIL_UNAVAILABLE (rail-provider timeout — internal PROVIDER_TIMEOUT distinction preserved on internal logs; the public surface emits the same RAIL_UNAVAILABLE by design — integration retry semantics are identical for either cause)
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:
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/orders/${ORDER_ID}/simulate/rate-lock-expired" \
  -H "x-api-key: ${SANDBOX_API_KEY}" \
  -H "idempotency-key: $(uuidgen)"
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, 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:
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/orders/${ORDER_ID}/simulate/conversion-failed" \
  -H "x-api-key: ${SANDBOX_API_KEY}" \
  -H "idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Provider rate stale"}'
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

StatusMeaningNext transitions
pendingOrder created; rate locked; awaiting executionsucceeded, failed, cancelled
succeededBoth conversion legs settled; crypto debited, fiat credited to the customer’s virtual account. The chained Withdrawal that delivers the fiat payout is tracked separately as a transaction.* event stream.Terminal
failedA conversion leg failed (e.g. via orders/:id/simulate/conversion-failed). Destination-payout failures — AML, rail — do not put the order in failed; they surface on the chained Withdrawal’s transaction.failed.Terminal
cancelledOrder cancelled by client or expired before executionTerminal
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

EventWhen it fires
order.createdOrder accepted, rate locked.
order.succeededOrder reached terminal succeeded state: crypto debited, conversion completed. The chained Withdrawal that delivers the fiat payout is a separate transaction.
order.failedOrder reached terminal failed state (e.g. orders/:id/simulate/conversion-failed). Payload carries reasonCode (INSUFFICIENT_FUNDS / PROVIDER_UNAVAILABLE / PROVIDER_REJECTED / INTERNAL_ERROR / CANCELLED). AML / rail failures on the destination payout do not surface here — they surface on transaction.failed for the chained Withdrawal.
order.cancelledOrder cancelled (expired or client-cancelled). Payload carries cancellationReason.
transaction.createdA chained Withdrawal is dispatched to deliver the fiat payout. The payload carries linkedOrderId pointing back at the parent OFFRAMP order.
transaction.failedThe chained Withdrawal failed (compliance reject, rail failure). Payload carries failureCode.
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 workflow 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:
  • AML_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.
If the OFFRAMP order itself fails (conversion aborted via orders/:id/simulate/conversion-failed), the failure surfaces on order.failed with reasonCode.

Sequence diagram

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

Errors

Full error catalog and failureCode reference