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

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

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

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](/v2/overview#organizations-in-the-route).

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

## Offramp flow

<Steps>
  <Step title="Discover assets">
    <Expandable title="Details">
      Either of these endpoints gives you the asset identifiers for a quote:

      * [GET /supported\_assets](/v2/api-reference/assets/list-supported-assets) 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](/v2/api-reference/lookup/list-stablebonds) 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.
      </Warning>
    </Expandable>
  </Step>

  <Step title="Register the wallet and the bank account: POST /organization/{organizationId}/payment_account">
    <Expandable title="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](/v2/api-reference/payment-accounts/register-payment-account) for the full schema.

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

      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}](/v2/api-reference/payment-accounts/get-payment-account), or listen for [`paymentAccountUpdated`](/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.
      </Note>
    </Expandable>
  </Step>

  <Step title="Create a quote: POST /organization/{organizationId}/quote">
    <Expandable title="Details">
      Quote from the wallet to the bank account. Both accounts must belong to the organization in the route. Set `sourceAsset` to the asset identifier and `destinationAsset` to the fiat currency. Quotes expire **2 minutes** after they are created (`expiresAt`). See [Create quote](/v2/api-reference/quotes/create-quote) for the full schema.

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

      <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](/v2/fees#partner-fees).
      </Tip>
    </Expandable>
  </Step>

  <Step title="Create the order: POST /organization/{organizationId}/order">
    <Expandable title="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](/v2/api-reference/orders/create-order) for the full schema.

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

      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",
          "reference": "1000043",
          "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.
    </Expandable>
  </Step>

  <Step title="Send the crypto">
    <Expandable title="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](/v2/api-reference/orders/refresh-funding-instructions). The new funding instructions arrive a moment later in an `orderUpdated` webhook and on GET. See [Stellar](/v2/guides/chains/stellar#stale-transactions-and-refresh).
      </Warning>

      <Note>
        **Embedded wallets** sign through their owner instead. See [Embedded Wallets](/v2/guides/embedded-wallets).
      </Note>
    </Expandable>
  </Step>

  <Step title="Track the order: GET /organization/{organizationId}/order/{id}">
    <Expandable title="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`](/v2/api-reference/webhook-events/orderupdated) with [POST /webhook\_subscription](/v2/api-reference/webhook-subscriptions/create-webhook-subscription). See [Get order](/v2/api-reference/orders/get-order) for the response schema.

      <Info>
        **No fiat simulation.** Offramps have no `fiat_received` step. Sending the crypto on the testnet is enough.
      </Info>

      <CodeGroup>
        ```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>
        ```
      </CodeGroup>
    </Expandable>
  </Step>
</Steps>

<Expandable title="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"
  }
  ```
</Expandable>

<Expandable title="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 and can include a `bridge` fee line.
</Expandable>

<Expandable title="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](/v2/guides/chains/stellar#offramps-and-swaps-fund-the-wallet-first) |

  See [Errors](/v2/errors) for every problem type.
</Expandable>


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