> ## Documentation Index
> Fetch the complete documentation index at: https://v2.docs.conduit.financial/llms.txt
> Use this file to discover all available pages before exploring further.

# OFFRAMP orders in sandbox

> Step-by-step guide for testing crypto-in to fiat-out orders: happy path, destination failure, compliance gates, rate-lock expiry, and webhooks

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](/sandbox/quickstart) first.
* A crypto wallet for the customer holding the source asset (e.g. USDC on Ethereum). See [Deposits](/sandbox/deposits) for injecting a synthetic balance.
* Base URL: `https://api.sandbox.conduit.financial`. Export your key:

```bash theme={null}
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). On a [deposit-funded order](/concepts/deposit-funded-orders) 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 ordinary `deposit` transaction you can read back.
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

<Note>
  `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).
</Note>

<Note>
  `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](#deposit-funded-offramp-no-source) below.
</Note>

<CodeGroup>
  ```bash curl theme={null}
  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",
        "purpose": "payment_for_goods_or_services",
        "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
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(`${process.env.SANDBOX_HOST}/v2/orders`, {
    method: "POST",
    headers: {
      "x-api-key": process.env.SANDBOX_API_KEY!,
      "idempotency-key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      clientReferenceId: "cref-offramp-1",
      source: {
        type: "wallet",
        id: process.env.WALLET_ID,
        asset: { code: "USDC", chain: "ethereum" },
      },
      destination: { type: "virtual_account", id: process.env.VA_ID },
      autoPayout: {
        rail: "fedwire",
        purpose: "payment_for_goods_or_services",
        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,
    }),
  });
  const { id: orderId } = await response.json();
  // orderId → "ord_..."
  ```

  ```python Python theme={null}
  import httpx, uuid, os

  response = httpx.post(
      f"{os.environ['SANDBOX_HOST']}/v2/orders",
      headers={
          "x-api-key": os.environ["SANDBOX_API_KEY"],
          "idempotency-key": str(uuid.uuid4()),
          "Content-Type": "application/json",
      },
      json={
          "clientReferenceId": "cref-offramp-1",
          "source": {
              "type": "wallet",
              "id": os.environ["WALLET_ID"],
              "asset": {"code": "USDC", "chain": "ethereum"},
          },
          "destination": {"type": "virtual_account", "id": os.environ["VA_ID"]},
          "autoPayout": {
              "rail": "fedwire",
              "purpose": "payment_for_goods_or_services",
              "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,
      },
  )
  order_id = response.json()["id"]
  # order_id → "ord_..."
  ```
</CodeGroup>

`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`.

```bash theme={null}
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](/sandbox/deposits):

<CodeGroup>
  ```bash curl theme={null}
  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" }
    }'
  ```

  ```typescript TypeScript theme={null}
  await fetch(
    `${process.env.SANDBOX_HOST}/v2/sandbox/customers/${customerId}/wallets/${walletId}/deposits/simulate`,
    {
      method: "POST",
      headers: {
        "x-api-key": process.env.SANDBOX_API_KEY!,
        "idempotency-key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        assetAmount: { code: "USDC", chain: "ethereum", amount: "100" },
      }),
    },
  );
  ```

  ```python Python theme={null}
  import httpx, os, uuid
  httpx.post(
      f"{os.environ['SANDBOX_HOST']}/v2/sandbox/customers/{customer_id}/wallets/{wallet_id}/deposits/simulate",
      headers={
          "x-api-key": os.environ["SANDBOX_API_KEY"],
          "idempotency-key": str(uuid.uuid4()),
          "Content-Type": "application/json",
      },
      json={"assetAmount": {"code": "USDC", "chain": "ethereum", "amount": "100"}},
  )
  ```
</CodeGroup>

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.

## 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](/concepts/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:

```bash theme={null}
export SENDER="0x8f3a1e5b9c2d4a6f8e0b1c3d5f7a9b1c3d5e7f90"

curl -X POST "https://api.sandbox.conduit.financial/v2/customers/${CUSTOMER_ID}/wallets/registered-addresses" \
  -H "x-api-key: ${SANDBOX_API_KEY}" \
  -H "idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "self_custody",
    "chain": "ethereum",
    "address": "'"${SENDER}"'",
    "selfCustodyAttestation": true
  }'
```

`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`):

```bash theme={null}
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-deposit-funded",
    "sourceAsset": { "code": "USDC", "chain": "ethereum" },
    "destination": { "type": "virtual_account", "id": "'"${VA_ID}"'" },
    "autoPayout": {
      "rail": "fedwire",
      "purpose": "intercompany",
      "recipient": { "...": "as in Step A above" }
    },
    "lockSide": "source",
    "amount": "100.000000"
  }'
```

`202 Accepted`. The response has **no `source` key** and carries `depositInstructions` instead:

```json theme={null}
{
  "id": "ord_...",
  "status": "pending",
  "autoExecute": true,
  "lockExpiresAt": "2026-01-16T09:30:00.000Z",
  "depositInstructions": [
    {
      "type": "crypto_address",
      "address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
      "chain": "ethereum",
      "asset": "USDC",
      "expiresAt": "2026-01-16T09:30:00.000Z"
    }
  ]
}
```

```bash theme={null}
export ORDER_ID="ord_..."
```

<Warning>
  `lockExpiresAt` here is the **funding deadline**, not a rate expiry, and it is
  **5 minutes** — it used to be 24 hours. Fund the order in the same run; an
  order left sitting between steps will already be `cancelled (expired)`.
</Warning>

### Step 2 — Fund the address

The funding address has no client-visible wallet id, so the order id is the handle:

```bash theme={null}
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/orders/${ORDER_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" },
    "sourceAddress": "'"${SENDER}"'"
  }'
```

`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, and `order.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.

| To test                                           | Do this                                                                                                    | What happens                                                                                                                                                                     |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Under-funding                                     | `amount` below the order's `totalDebit`                                                                    | The order never reaches its total, stays `pending`, and expires at the deadline. The deposit stays `completed`; the funds then go back to the sender as a `deposit_return`.      |
| Over-funding                                      | `amount` above the order's `totalDebit`                                                                    | The order executes; the surplus goes back to the sender once the order settles, as a `deposit_return` for the surplus only.                                                      |
| An unregistered sender                            | Omit `sourceAddress`, or pass one you never registered                                                     | The order never moves, but the transfer is still readable: the deposit reads `failed` (nothing was ever credited) and a `deposit_return` names it via `returnOf`.                |
| An unclaimed transfer                             | Expire the order first with `POST /v2/sandbox/orders/${ORDER_ID}/simulate/rate-lock-expired`, then fund it | The order stays `cancelled (expired)`; the funds fund nothing and go back to the sender as a `deposit_return`.                                                                   |
| A compliance hold on the funding transfer         | Add `"outcome": "frozen"` to the body                                                                      | The funds fund nothing, so the order cannot reach its total and expires. The deposit's own transaction reads `failed` with a generic reason and no failure code.                 |
| Resolving a transfer held for a compliance review | `POST /v2/sandbox/orders/${ORDER_ID}/deposits/simulate/compliance-decision` with `approve` or `reject`     | `approve` releases the funds to the order; `reject` holds them permanently and the order expires unfunded. `404 SANDBOX_ORDER_NO_PARKED_FUNDING` when nothing is currently held. |

<Warning>
  Reconcile against the order's status first — a transfer that funds nothing
  never moves the order, whatever the deposit reads. The deposit and its
  `deposit_return` are both readable on `GET /v2/transactions`, but a deposit
  that never funded anything reads `failed` with no further detail, so the
  order is still the clearer signal for "did this work".
</Warning>

## 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; 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.

<CodeGroup>
  ```bash curl theme={null}
  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-compliance",
      "source": {
        "type": "wallet",
        "id": "'"${WALLET_ID}"'",
        "asset": { "code": "USDC", "chain": "ethereum" }
      },
      "destination": { "type": "virtual_account", "id": "'"${VA_ID}"'" },
      "autoPayout": {
        "rail": "fedwire",
        "purpose": "payment_for_goods_or_services",
        "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
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(`${process.env.SANDBOX_HOST}/v2/orders`, {
    method: "POST",
    headers: {
      "x-api-key": process.env.SANDBOX_API_KEY!,
      "idempotency-key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      clientReferenceId: "cref-offramp-compliance",
      source: {
        type: "wallet",
        id: process.env.WALLET_ID,
        asset: { code: "USDC", chain: "ethereum" },
      },
      destination: { type: "virtual_account", id: process.env.VA_ID },
      autoPayout: {
        rail: "fedwire",
        purpose: "payment_for_goods_or_services",
        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,
    }),
  });
  ```

  ```python Python theme={null}
  import httpx, uuid, os

  response = httpx.post(
      f"{os.environ['SANDBOX_HOST']}/v2/orders",
      headers={
          "x-api-key": os.environ["SANDBOX_API_KEY"],
          "idempotency-key": str(uuid.uuid4()),
          "Content-Type": "application/json",
      },
      json={
          "clientReferenceId": "cref-offramp-compliance",
          "source": {
              "type": "wallet",
              "id": os.environ["WALLET_ID"],
              "asset": {"code": "USDC", "chain": "ethereum"},
          },
          "destination": {"type": "virtual_account", "id": os.environ["VA_ID"]},
          "autoPayout": {
              "rail": "fedwire",
              "purpose": "payment_for_goods_or_services",
              "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,
      },
  )
  ```
</CodeGroup>

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 value   | `failureCode` on the chained Withdrawal `transaction.failed` |
| ------------------------ | ------------------------------------------------------------ |
| `SANDBOX_AML_REJECTED`   | `COMPLIANCE_REVIEW_REJECTED`                                 |
| `SANDBOX_AML_SANCTIONED` | `COMPLIANCE_REVIEW_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 `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), 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 digits | `failureCode` on the Withdrawal                                                                                                                                                                                                              |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `94009001`                    | `RAIL_POLICY_REJECTED`                                                                                                                                                                                                                       |
| `94009002`                    | `INSUFFICIENT_FUNDS_AT_SETTLE`                                                                                                                                                                                                               |
| `94009003`                    | `RAIL_UNAVAILABLE`                                                                                                                                                                                                                           |
| `94009004`                    | `RAIL_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:

```bash theme={null}
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](/sandbox/onramps) for a worked example.

<Note>
  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.
</Note>

## 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:

```bash theme={null}
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](/sandbox/conversions) for the full endpoint reference.

## Order lifecycle states

| Status      | Meaning                                                                                                                                                                                                                           | Next transitions                   |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| `pending`   | Order created; rate locked; awaiting execution                                                                                                                                                                                    | `succeeded`, `failed`, `cancelled` |
| `succeeded` | Both 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                           |
| `failed`    | A conversion leg failed (e.g. via `orders/:id/simulate/conversion-failed`). Destination-payout failures — compliance, rail — do **not** put the order in `failed`; they surface on the chained Withdrawal's `transaction.failed`. | Terminal                           |
| `cancelled` | Order cancelled by client or expired before execution                                                                                                                                                                             | Terminal                           |

<Note>
  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.
</Note>

## Webhook events

| Event                 | When it fires                                                                                                                                                                                                                                                                                                                                                              |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `order.created`       | Order accepted, rate locked.                                                                                                                                                                                                                                                                                                                                               |
| `order.succeeded`     | Order reached terminal `succeeded` state: crypto debited, conversion completed. The chained Withdrawal that delivers the fiat payout is a separate transaction.                                                                                                                                                                                                            |
| `order.failed`        | Order reached terminal `failed` state (e.g. `orders/:id/simulate/conversion-failed`). Payload carries `reasonCode` (`INSUFFICIENT_FUNDS` / `PROVIDER_UNAVAILABLE` / `PROVIDER_REJECTED` / `INTERNAL_ERROR` / `CANCELLED`). Compliance / rail failures on the destination payout do **not** surface here — they surface on `transaction.failed` for the chained Withdrawal. |
| `order.cancelled`     | Order cancelled (expired or client-cancelled). Payload carries `cancellationReason`.                                                                                                                                                                                                                                                                                       |
| `transaction.created` | A chained Withdrawal is dispatched to deliver the fiat payout. The payload carries `linkedOrderId` pointing back at the parent OFFRAMP order.                                                                                                                                                                                                                              |
| `transaction.failed`  | The chained Withdrawal failed (compliance reject, rail failure). Payload carries `failureCode`.                                                                                                                                                                                                                                                                            |

<Note>
  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.
</Note>

## Errors

See [Errors](/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.

If the OFFRAMP order itself fails (conversion aborted via `orders/:id/simulate/conversion-failed`), the failure surfaces on `order.failed` with `reasonCode`.

## Sequence diagram

```mermaid theme={null}
sequenceDiagram
    participant You as Your backend
    participant API as Conduit sandbox
    participant WH as Your webhook endpoint

    You->>API: POST /v2/orders (source=wallet, destination=virtual_account)
    API-->>You: 202 { id: "ord_..." }

    You->>API: POST /v2/sandbox/customers/{customerId}/wallets/{walletId}/deposits/simulate
    API-->>You: 202 { chain, txHash }

    Note over API: Mock provider auto-finalizes conversion legs
    Note over You,API: Poll GET /v2/orders/{orderId} to observe progress (optional)

    API-->>WH: order.succeeded
    API-->>WH: transaction.created (chained Withdrawal; linkedOrderId = ord_...)
    API-->>WH: transaction.failed (on compliance / rail failure) — OR — transaction.completed (happy path)
```

## Related pages

<CardGroup cols={2}>
  <Card title="ONRAMP orders" href="/sandbox/onramps">
    Fiat-in to crypto-out — the mirror flow
  </Card>

  <Card title="Deposits" href="/sandbox/deposits">
    Injecting synthetic crypto balances into a customer wallet
  </Card>

  <Card title="Conversions" href="/sandbox/conversions">
    FX conversion leg mechanics and failure scenarios
  </Card>

  <Card title="Deposit-Funded Orders" href="/concepts/deposit-funded-orders">
    Orders created with no source, and the funds-returned caveats
  </Card>

  <Card title="Errors" href="/errors">
    Full error catalog and failureCode reference
  </Card>
</CardGroup>
