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

# Conversions in sandbox

> Test FX conversion failure scenarios inside ONRAMP and OFFRAMP orders using the orders/:id/simulate/conversion-failed endpoint

A **crypto-to-crypto conversion is its own standalone order**: `POST /v2/orders` with a wallet `source` and a wallet `destination` resolves to `type: conversion` and moves a customer's balance from one chain to another.

## The standalone crypto conversion order

To move a customer's crypto balance from one chain to another — for example USDC on Ethereum to USDC on Polygon — create an order with a **wallet** `source` and a **wallet** `destination`, each naming its own chain. Conduit resolves it to `type: conversion`. There is no `type` field to send; the source/destination shapes determine it.

```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 '{
    "source": {
      "type": "wallet",
      "id": "wlt_...",
      "asset": { "code": "USDC", "chain": "ethereum" }
    },
    "destination": {
      "type": "wallet",
      "id": "wlt_...",
      "asset": { "code": "USDC", "chain": "polygon" }
    },
    "lockSide": "source",
    "amount": "1.00",
    "autoExecute": true
  }'
```

`202 Accepted` with the order `pending`. It runs two on-chain legs: `wallet_to_ops` moves the source asset out of the customer's wallet, and `ops_to_wallet` delivers the destination asset. Terminal status arrives on `order.succeeded` / `order.failed`.

<Warning>
  **On the real-testnet sandbox this conversion is real on-chain and a non-custodial source needs a signature.** The `wallet_to_ops` source leg leaves the customer's **non-custodial** wallet, so it parks at `transaction.awaiting_signature` — approve with a real passkey or a machine stamp at `POST /v2/signing-requests/:id/approve`. Both legs broadcast real testnet `txHash`es you can verify on each chain's explorer, and finality takes minutes. Fund the source wallet from a faucet — a `deposits/simulate` balance cannot be broadcast. See [Use a real testnet](/sandbox/testnet) and [Machine-signer stamping](/guides/machine-signer-stamping).
</Warning>

## Conversions inside onramps and offramps

Every ONRAMP and OFFRAMP order also includes a conversion leg that exchanges the source asset for the destination asset. That embedded leg runs automatically after the source leg settles — you do not create it directly. The rest of this page explains how to drive conversion failure outcomes for either order shape in sandbox.

<Note>
  This page applies to both order shapes. An order that names a `source` is funded from a balance the customer already holds; an order created with **no** `source` — sending `sourceAsset` instead — is funded at an address Conduit publishes on the order, by a real testnet transfer. Both then run the same conversion leg and fail the same way. See [Deposit-Funded Orders](/concepts/deposit-funded-orders) and the [deposit-funded walkthrough](/sandbox/offramps).
</Note>

<Note>
  The `reason` field is optional (max 500 chars). When supplied, it is recorded as the note on the cancelled source transfer, not on the order — `order.failed.failureMessage` and `GET /v2/orders/:id` carry the standard text for the failure code, exactly as in production. Your `reason` text does not appear on the order or on its transactions.
</Note>

## Customers in the United States

On live, Conduit does not convert for a customer in the United States. This is a business that operates in the United States, or that is registered there and declares no operating country, or an individual who lives there. A crypto or fiat conversion, an onramp or an offramp for such a customer returns [`403 CAPABILITY_SUSPENDED`](/errors#capability-suspended) when you create or execute the order.

Sandbox does not apply this refusal, so you can test every order shape with any customer. To see the refusal in sandbox, restrict the customer:

```bash curl theme={null}
curl -X POST https://api.sandbox.conduit.financial/v2/sandbox/customers/$CUSTOMER_ID/simulate/restrict \
  -H "x-api-key: $SANDBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restrictionType": "conversions_disabled" }'
```

The customer's next conversion, onramp or offramp, crypto or fiat, returns the same `403 CAPABILITY_SUSPENDED` as on live. Remove the restriction with `POST /v2/sandbox/customers/{customerId}/simulate/lift-restriction` and the same body.

## How to fail a conversion

Use the `orders/:id/simulate/conversion-failed` endpoint to force a conversion to a failed terminal state. The endpoint is timing-independent: call it at any point after order creation. If the conversion is already running, it is driven to failed immediately; otherwise the failure is armed and applied as soon as the conversion starts. The order is driven to `failed` and `order.failed` is emitted.

<CodeGroup>
  ```bash curl 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"}'
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch(
    `${process.env.SANDBOX_HOST}/v2/sandbox/orders/${orderId}/simulate/conversion-failed`,
    {
      method: "POST",
      headers: {
        "x-api-key": process.env.SANDBOX_API_KEY!,
        "idempotency-key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ reason: "Provider rate stale" }),
    },
  );
  ```

  ```python Python theme={null}
  import httpx, os, uuid
  r = httpx.post(
      f"{os.environ['SANDBOX_HOST']}/v2/sandbox/orders/{order_id}/simulate/conversion-failed",
      headers={
          "x-api-key": os.environ["SANDBOX_API_KEY"],
          "idempotency-key": str(uuid.uuid4()),
      },
      json={"reason": "Provider rate stale"},
  )
  ```
</CodeGroup>

Returns `200 OK` with the order at its current state. Returns `404` if the order is not found. Replays against an already-terminal order return `200` with the current resource (idempotent).

<Note>
  **Rate-lock expiry timing.** `POST
      /v2/sandbox/orders/:id/simulate/rate-lock-expired` returns `200 OK` with the
  order at its current state. The order reaches `cancelled (expired)` within
  seconds. There is no fixed delay: wait for the `order.cancelled` webhook.
</Note>

There is no create-time outcome locking for conversions. This endpoint is the only way to force a conversion failure on demand, and it works at any point after the order exists. A conversion can also fail on its own when the transfer that delivers the destination asset does not complete; the order reaches `failed` the same way, with a `failureMessage` Conduit writes.

## Webhook events

Conversion outcomes surface on the order's webhook topic, and the order's underlying transaction fires its own events — each carrying `linkedOrderId` so you can join it back to the order.

| Event | When it fires |
| - | - |
| `order.created` | The order is accepted and begins executing. |
| `order.succeeded` | The full order completed successfully (source settled, conversion ran, destination credited). Terminal. |
| `order.failed` | The order failed. `reasonCode` (`insufficient_funds` / `provider_unavailable` / `provider_rejected` / `internal_error` / `cancelled` / `insufficient_onchain_balance` / `source_account_unavailable`) names the failure category. Conversion failures surface here. |
| `order.cancelled` | The order was cancelled before execution completed. `reason` is `expired` or `client_cancelled`. |
| `transaction.created` | The order's transaction is initiated. |
| `transaction.awaiting_signature` | The source leg leaves a non-custodial wallet and needs a signature before it can broadcast. |
| `transaction.processing` | The transaction a conversion executes moves into active processing. |
| `transaction.completed` | The transaction settles. |

See the [Webhooks reference](/webhooks) for full payload schemas.

## Errors

Conversion failures produce `order.failed` on the order. The payload carries `reasonCode` (`insufficient_funds` / `provider_unavailable` / `provider_rejected` / `internal_error` / `cancelled` / `insufficient_onchain_balance` / `source_account_unavailable`) describing the failure category. See [Error codes](/errors) for the broader catalog used by other event types.

A cross-chain conversion delivers its destination asset from sandbox's shared testnet liquidity, so it draws on the same per-chain, per-UTC-day cap an onramp does. A conversion whose destination leg would carry the customer's net draw for the day over the cap on the destination chain returns [`422 SANDBOX_ONRAMP_DAILY_CAP_EXCEEDED`](/errors#sandbox-onramp-daily-cap-exceeded) at creation or execution. The default is small, so the examples above use `"amount": "1.00"` — an amount a new customer can execute. See [ONRAMP orders — Errors](/sandbox/onramps#errors) for the full cap semantics, how the cap resets at UTC midnight, and how a settled offramp frees headroom sooner.

## See also

* [Sandbox overview](/sandbox/overview)
* [Webhooks reference](/webhooks)
* [Error codes](/errors)
* [Onramps in sandbox](/sandbox/onramps)
* [Offramps in sandbox](/sandbox/offramps)
* [Deposit-funded orders](/concepts/deposit-funded-orders)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.