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

# Offramps

> How to test offramp flows in the sandbox environment

> [!NOTE]
>
> **Prerequisite:** Your organization must be approved before it can quote, and the wallet must hold the asset it sells. See [KYC Overview](https://docs.etherfuse.com/v2/guides/onboarding.md) for what verification requires, and [KYC WebSDK](https://docs.etherfuse.com/v2/guides/kyc-websdk.md) for the hosted flow.

> [!NOTE]
>
> **Redemptions do not depend on how much was onramped.** A customer can sell any amount of a stablebond up to their current balance. We do not pair buys with sells or track lots, so partial redemptions and tokens bought elsewhere both work.

An offramp moves crypto from a wallet to a bank account. In v2 both sides are payment accounts: you register the wallet and the bank account, quote between them, create the order, and send the crypto.

Every call below runs for one organization, set as `$ORGANIZATION_ID` in the examples. Use your own organization's id from `GET /me`, or a customer organization's id when you ramp for that customer. Register the accounts, quote and order under the same id. See [Organizations in the route](https://docs.etherfuse.com/v2/overview.md#organizations-in-the-route).

```bash theme={null}
export ORGANIZATION_ID=<organization_id>
```

## Offramp flow

**1. Discover assets**

**Details**

Either of these endpoints gives you the asset identifiers for a quote:

* [GET /supported\_assets](https://docs.etherfuse.com/v2/api-reference/assets/list-supported-assets.md) requires auth. It returns every market and token a quote can use. Pass `paymentAccountId` to see what one of your accounts can use, or `chain` for one chain.
* [GET /lookup/stablebonds](https://docs.etherfuse.com/v2/api-reference/lookup/list-stablebonds.md) is public. It returns every stablebond on every chain, with pricing and supply data.

Use a token's `asset` from the first, or the `identifier` from the second, as `sourceAsset` in the quote.

> [!WARNING]
>
> **Asset identifier format differs by chain:**
>
> * **Solana:** Base58 mint address (for example `AvvetPGuuB5FD5m86fpw3LtDKyQoUFT1mG9WarNQLW4q`)
> * **Stellar:** `CODE:ISSUER` (for example `CETES:GC3CW7...`)
> * **EVM (Base, Polygon, Monad, Avalanche):** the raw contract address only (for example `0xcC77c598d42f2f78Beb42C91d12B9d4041a5cE29`)
>
> A `SYMBOL:0x...` identifier on an EVM chain fails.

**2. Register the wallet and the bank account: POST /organization/\{organizationId}/payment\_account**

**Details**

Register the wallet that sends the crypto and the bank account that receives the payout. Each call returns the account's `id` and its `rails`. See [Register payment account](https://docs.etherfuse.com/v2/api-reference/payment-accounts/register-payment-account.md) for the full schema.

```bash Wallet theme={null}
curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/payment_account \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "wallet",
    "label": "Treasury wallet",
    "accountDetails": {
      "kind": "address",
      "address": "0xf82567432381D1326484E5A9140ABCa4294f82D0",
      "chain": "base"
    }
  }'
```

```bash Bank account (SPEI) theme={null}
curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/payment_account \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "bank",
    "label": "Operating account",
    "country": "MX",
    "currency": "MXN",
    "accountDetails": { "kind": "clabe", "clabe": "012180000000000002" }
  }'
```

For an offramp, the wallet needs `source` ready on its chain rail, and the bank account needs `destination` ready on its fiat rail (`spei`). Read the account again with [GET /organization/\{organizationId}/payment\_account/\{id}](https://docs.etherfuse.com/v2/api-reference/payment-accounts/get-payment-account.md), or listen for [`paymentAccountUpdated`](https://docs.etherfuse.com/v2/api-reference/webhook-events/paymentaccountupdated).

> [!NOTE]
>
> A bank account route needs holder details such as `accountHolderName` and `taxId`. We fill them from the verified identity of the organization in the route, so pass the customer's organization id to register a customer's bank account. If a field is still missing, we return `422` `registration-info-required` with the fields in `missing`. Send them under `registrationInfo` and retry.

**3. Create a quote: POST /organization/\{organizationId}/quote**

**Details**

Quote from the wallet to the bank account. Each account must belong to the organization in the route or to your API key's organization. Set `sourceAsset` to the asset identifier and `destinationAsset` to the fiat currency. Quotes expire **2 minutes** after they are created (`expiresAt`). See [Create quote](https://docs.etherfuse.com/v2/api-reference/quotes/create-quote.md) for the full schema.

```bash Sandbox theme={null}
curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/quote \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "type": "stored", "id": "<wallet_payment_account_id>" },
    "destination": { "type": "stored", "id": "<bank_payment_account_id>" },
    "sourceAmount": "50",
    "sourceAsset": "0xcC77c598d42f2f78Beb42C91d12B9d4041a5cE29",
    "destinationAsset": "MXN"
  }'
```

```bash Production theme={null}
curl -X POST https://api.etherfuse.com/organization/$ORGANIZATION_ID/quote \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "type": "stored", "id": "<wallet_payment_account_id>" },
    "destination": { "type": "stored", "id": "<bank_payment_account_id>" },
    "sourceAmount": "50",
    "sourceAsset": "0x834df4C1d8f51Be24322E39e4766697BE015512F",
    "destinationAsset": "MXN"
  }'
```

> [!TIP]
>
> **Partner fees:** We apply your organization's partner fee automatically. To set it for one quote, add `"partnerFeeBpsOverride": 100`. We cap values above 500. See [Partner fees](https://docs.etherfuse.com/v2/fees.md#partner-fees).

**4. Create the order: POST /organization/\{organizationId}/order**

**Details**

Send the `quoteId`. The response is `201` with the full order, and the order's `id` equals the `quoteId`. Sending the same `quoteId` again returns the existing order with `200`. See [Create order](https://docs.etherfuse.com/v2/api-reference/orders/create-order.md) for the full schema.

```bash Sandbox theme={null}
curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/order \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "quoteId": "<quote_id>" }'
```

```bash Production theme={null}
curl -X POST https://api.etherfuse.com/organization/$ORGANIZATION_ID/order \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "quoteId": "<quote_id>" }'
```

On an offramp, `fundingInstructions.rail` is the wallet's chain, and `fundingInstructions.destination` says where to send the crypto:

```json theme={null}
{
  "fundingInstructions": {
    "rail": "stellar",
    "destination": {
      "expiresAt": null,
      "address": "GABPM7AXX...",
      "memo": "<memo>",
      "preparedTransaction": {
        "transaction": "AAAAAgAAAABk...base64_xdr...",
        "refreshAfter": "2026-09-28T18:05:02Z"
      }
    },
    "fundingDeadline": "2026-09-28T18:05:02Z"
  }
}
```

| Field | Meaning |
| - | - |
| `address` | The address to send the crypto to. |
| `memo` | The memo to include in the transfer. Absent when none is needed. |
| `preparedTransaction` | A transaction we built for the customer to sign and submit. Absent when there is none. |
| `preparedTransaction.refreshAfter` | After this time the transaction can no longer land. On Solana it is about 60 seconds after we build it, when the blockhash expires. On Stellar it is the order's `fundingDeadline`. Null on EVM chains, where the transaction has no time limit. A Stellar or EVM transaction also goes stale when the wallet's sequence number or nonce moves. |

`destination` is absent until it is available. You receive it in an `orderUpdated` event, or you can read the order again.

An offramp's `fundingInstructions` has no `reference`. The crypto goes to an address for this order, so we don't need one to match it.

**5. Send the crypto**

**Details**

Offramps need an on-chain action from the customer. Fund the order before `fundingDeadline` in one of two ways:

1. **Sign the prepared transaction.** When `preparedTransaction` is present, deserialize `transaction`, sign it with the customer's wallet, and submit it to the network.
2. **Send a transfer yourself.** Send the order's `source.amount` of `source.asset` to `address`, with `memo` when it is present.

> [!WARNING]
>
> **Prepared transactions go stale.** A Solana transaction goes stale after `refreshAfter`, when its blockhash expires. A Stellar or EVM transaction goes stale when the wallet sends another transaction first, because its sequence number or nonce moves. In either case, call [POST /organization/\{organizationId}/order/\{id}/funding\_instructions/refresh](https://docs.etherfuse.com/v2/api-reference/orders/refresh-funding-instructions.md). The new funding instructions arrive a moment later in an `orderUpdated` webhook and on GET. See [Stellar](https://docs.etherfuse.com/v2/guides/chains/stellar.md#stale-transactions-and-refresh).

> [!NOTE]
>
> **Embedded wallets** sign through their owner instead. See [Embedded Wallets](https://docs.etherfuse.com/v2/guides/embedded-wallets.md).

**6. Track the order: GET /organization/\{organizationId}/order/\{id}**

**Details**

We detect the transfer on-chain and send the fiat payout. The status moves `created` → `funded` (crypto received) → `completed` (fiat sent). Read the order, or subscribe to [`orderUpdated`](https://docs.etherfuse.com/v2/api-reference/webhook-events/orderupdated) with [POST /webhook\_subscription](https://docs.etherfuse.com/v2/api-reference/webhook-subscriptions/create-webhook-subscription.md). See [Get order](https://docs.etherfuse.com/v2/api-reference/orders/get-order.md) for the response schema.

> [!NOTE]
>
> **No fiat simulation.** Offramps have no `fiat_received` step. Sending the crypto on the testnet is enough.

```bash Sandbox theme={null}
curl -H "Authorization: <api_key>" \
  https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/order/<order_id>
```

```bash Production theme={null}
curl -H "Authorization: <api_key>" \
  https://api.etherfuse.com/organization/$ORGANIZATION_ID/order/<order_id>
```

**Chain-specific quote examples**

The destination is always the bank account. Only the wallet and `sourceAsset` change.

**Solana:** use the Base58 mint address:

```json theme={null}
{
  "source": { "type": "stored", "id": "<solana_wallet_payment_account_id>" },
  "destination": { "type": "stored", "id": "<bank_payment_account_id>" },
  "sourceAmount": "50",
  "sourceAsset": "AvvetPGuuB5FD5m86fpw3LtDKyQoUFT1mG9WarNQLW4q",
  "destinationAsset": "MXN"
}
```

**Stellar:** use `CODE:ISSUER`:

```json theme={null}
{
  "source": { "type": "stored", "id": "<stellar_wallet_payment_account_id>" },
  "destination": { "type": "stored", "id": "<bank_payment_account_id>" },
  "sourceAmount": "50",
  "sourceAsset": "CETES:GC3CW7...",
  "destinationAsset": "MXN"
}
```

**Base, Polygon, Monad or Avalanche:** use the raw contract address. This example uses Base; get the address for your chain from `GET /supported_assets`:

```json theme={null}
{
  "source": { "type": "stored", "id": "<evm_wallet_payment_account_id>" },
  "destination": { "type": "stored", "id": "<bank_payment_account_id>" },
  "sourceAmount": "50",
  "sourceAsset": "0xcC77c598d42f2f78Beb42C91d12B9d4041a5cE29",
  "destinationAsset": "MXN"
}
```

**Which assets each chain supports**

| Source asset | Chains |
| - | - |
| Stablebond (for example CETES) | Solana, Stellar, Base, Polygon, Monad, Avalanche |
| Stablecoin (for example USDC) | Solana, Stellar, Monad |

We redeem stablebonds on Solana, so a stablebond offramp from another chain bridges the asset first. We pay that bridge, so the quote has no `bridge` fee line.

**Common errors**

| Error | Cause | Fix |
| - | - | - |
| `409` `organization-not-approved` | Your organization is not approved | Complete verification, then quote again |
| `409` `payment-account-not-ready` | The wallet is not `ready` as a `source`, or the bank account is not `ready` as a `destination` | Check each account's `rails` and wait for the direction to become `ready` |
| `400` `quote-expired` | The quote is older than 2 minutes, or does not exist | Create a new quote |
| `400` `amount-out-of-range` | The amount is outside a rail's `minAmount` or `maxAmount` | Change the amount |
| `422` `quote-unavailable` | We cannot price the route right now | Retry with backoff |
| Quote rejected for a stablecoin on Base or Polygon | We quote stablecoins only on Solana, Stellar and Monad | Sell a stablebond, or use a wallet on one of those chains |
| `tx_bad_seq` (Stellar), a nonce error (EVM), or an expired blockhash (Solana) | The prepared transaction went stale before submission | Refresh the funding instructions and sign the new transaction |
| Stellar transfer fails | The wallet does not hold enough XLM for the network fee | Fund the wallet with XLM. See [Stellar](https://docs.etherfuse.com/v2/guides/chains/stellar.md#offramps-and-swaps-fund-the-wallet-first) |

See [Errors](https://docs.etherfuse.com/v2/errors.md) for every problem type.


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