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

# Onramps

> How to test onramp flows in the sandbox environment

> [!NOTE]
>
> **Prerequisite:** Your organization must be approved before it can quote. 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.

An onramp moves fiat from a bank account to a wallet. In v2 both sides are payment accounts: you register the bank account and the wallet, quote between them, create the order, and fund it.

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

## Onramp 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 `destinationAsset` 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.

Each chain has its own setup (trustlines, gas). See the chain guides: [Solana](https://docs.etherfuse.com/v2/guides/chains/solana.md), [Stellar](https://docs.etherfuse.com/v2/guides/chains/stellar.md), [Base](https://docs.etherfuse.com/v2/guides/chains/base.md), [Polygon](https://docs.etherfuse.com/v2/guides/chains/polygon.md), [Monad](https://docs.etherfuse.com/v2/guides/chains/monad.md), [Avalanche](https://docs.etherfuse.com/v2/guides/chains/avalanche.md).

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

**Details**

Register the bank account the customer pays from and the wallet that receives the crypto. 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 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" }
  }'
```

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

You can quote a direction only when it is `ready`. For an onramp, the bank account needs `source` ready on its fiat rail (`spei`), and the wallet needs `destination` ready on its chain rail. 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. Use the dummy RFC `XAXX010101000` in sandbox.

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

**Details**

Quote from the bank account to the wallet. Each account must belong to the organization in the route or to your API key's organization. Set `sourceAsset` to the fiat currency (`MXN` or `BRL`) and `destinationAsset` to the asset identifier. 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": "<bank_payment_account_id>" },
    "destination": { "type": "stored", "id": "<wallet_payment_account_id>" },
    "sourceAmount": "100",
    "sourceAsset": "MXN",
    "destinationAsset": "0xcC77c598d42f2f78Beb42C91d12B9d4041a5cE29"
  }'
```

```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": "<bank_payment_account_id>" },
    "destination": { "type": "stored", "id": "<wallet_payment_account_id>" },
    "sourceAmount": "100",
    "sourceAsset": "MXN",
    "destinationAsset": "0x834df4C1d8f51Be24322E39e4766697BE015512F"
  }'
```

The response has the `quoteId`, `sourceAmount`, `destinationAmount`, both rails, `rateBeforeFees`, `rateAfterFees`, the total fee (`feeBps`, `feeAmount`) and the fee lines in `fees[]`.

> [!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).

> [!NOTE]
>
> In sandbox, onramp quotes are limited to a `sourceAmount` of 500. Larger amounts return `400` `sandbox-limit-exceeded`.

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

**Details**

Send the `quoteId`. The quote carries the accounts, rails, assets and amounts, so you do not repeat them. The response is `201` with the full order, and the order's `id` equals the `quoteId`. 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>", "memo": "Invoice 1042" }'
```

```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>", "memo": "Invoice 1042" }'
```

`memo` is optional, up to 255 characters. Sending the same `quoteId` again returns the existing order with `200`, so a retry never creates a second order. See [Idempotency](https://docs.etherfuse.com/v2/idempotency.md).

`fundingInstructions` tells the customer how to pay:

```json theme={null}
{
  "fundingInstructions": {
    "rail": "spei",
    "reference": "1000042",
    "destination": {
      "expiresAt": null,
      "clabe": "646180000000000009",
      "bankName": "Ejemplo Banco",
      "recipientName": "Ejemplo Receptor"
    },
    "fundingDeadline": "2026-09-29T17:05:02Z"
  }
}
```

On `spei`, `destination` gives the CLABE to pay. On `pix`, it gives a `brCode`. The `201` response from creating the order has no `destination` yet. The CLABE, `bankName` and `recipientName` (or the `brCode`) arrive a moment later: you receive them in an `orderUpdated` event, or you can read the order again with [GET /organization/\{organizationId}/order/\{id}](https://docs.etherfuse.com/v2/api-reference/orders/get-order.md).

On `spei`, ask the customer to put `reference` in the transfer's referencia numérica. We match the deposit to the order by it.

**5. Simulate the fiat deposit: POST /organization/\{organizationId}/order/\{id}/fiat\_received**

**Details**

In production, the customer pays the funding destination before `fundingDeadline`. In sandbox, simulate the deposit with [Simulate fiat received](https://docs.etherfuse.com/v2/api-reference/sandbox/simulate-fiat-received.md). Send the order's `source.amount`.

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

We return `200` with an empty body, and the order continues as it would after a real deposit.

> [!NOTE]
>
> This endpoint works only in sandbox. In production it returns `403``forbidden`. It returns `422``unsupported` when the order's source is not a `spei` or `pix` bank account.

> [!WARNING]
>
> **The customer must send the exact order amount.** Pay the `source.amount` from the order to the funding destination.

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

**Details**

Read the order, or subscribe to `orderUpdated` with [POST /webhook\_subscription](https://docs.etherfuse.com/v2/api-reference/webhook-subscriptions/create-webhook-subscription.md). Each [`orderUpdated`](https://docs.etherfuse.com/v2/api-reference/webhook-events/orderupdated) event carries the full order. See [Get order](https://docs.etherfuse.com/v2/api-reference/orders/get-order.md) for the response schema.

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

The status moves `created` → `funded` → `completed`. A Stellar onramp can stop at `claimable` first; see [Stellar: wallets without a trustline](#stellar-wallets-without-a-trustline). An order can also end `failed` (see `failure`), `canceled` or `refunded`.

**Chain-specific quote examples**

The source is always the bank account. Only the wallet and `destinationAsset` change.

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

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

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

```json theme={null}
{
  "source": { "type": "stored", "id": "<bank_payment_account_id>" },
  "destination": { "type": "stored", "id": "<stellar_wallet_payment_account_id>" },
  "sourceAmount": "100",
  "sourceAsset": "MXN",
  "destinationAsset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
}
```

**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": "<bank_payment_account_id>" },
  "destination": { "type": "stored", "id": "<evm_wallet_payment_account_id>" },
  "sourceAmount": "100",
  "sourceAsset": "MXN",
  "destinationAsset": "0xcC77c598d42f2f78Beb42C91d12B9d4041a5cE29"
}
```

**Which assets each chain supports**

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

We issue stablebonds on Solana and bridge them to the wallet's chain. We pay that bridge, so a stablebond onramp to another chain has no `bridge` fee line. For an `avalanche` wallet, read the available assets from [GET /supported\_assets?chain=avalanche](https://docs.etherfuse.com/v2/api-reference/assets/list-supported-assets.md).

**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 bank account is not `ready` as a `source`, or the wallet is not `ready` as a `destination` | Check each account's `rails` and wait for the direction to become `ready` |
| `422` `registration-info-required` | The bank account route needs holder fields we do not hold | Send the fields in `missing` under `registrationInfo` |
| `400` `sandbox-limit-exceeded` | The sandbox onramp amount is above 500 | Use a smaller amount |
| `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 | Quote a stablebond, or use a wallet on one of those chains |
| `422` `unsupported` | We cannot quote this route | Change the asset or the wallet |

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

***

## Stellar: wallets without a trustline

A Stellar wallet needs an on-chain account and a trustline before it can hold an asset. When the destination wallet has neither, we still deliver: we send the asset as a **claimable balance**, and the customer claims it.

### How it works

1. **Quote as usual.** The wallet is the quote's destination, so we check it on-chain. If it needs setup, the quote's `feeBps` and `feeAmount` include a one-time cost to cover the XLM for the account and the trustline reserve.

2. **Create and fund the order as usual.**

3. **The order becomes `claimable`.** We deliver the claimable balance, and the order's `claimInstructions` describes it:

   ```json theme={null}
   {
     "id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
     "status": "claimable",
     "claimInstructions": {
       "kind": "stellarClaimableBalance",
       "balanceId": "00000000abc123...",
       "asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
       "amount": "54.1200000",
       "claimant": "GDUKMGUGD3V6VXTU2RLAUM7A2FABLMHCPWTMDHKP7HHJ6FCZKEY4PVWL",
       "expiresAt": null
     }
   }
   ```

4. **Prepare the claim.** Call [POST /organization/\{organizationId}/order/\{id}/claim](https://docs.etherfuse.com/v2/api-reference/orders/prepare-claim.md) with no body. It returns `202` with no body. A few seconds later the claim arrives in an `orderUpdated` webhook and on [GET .../order/\{id}](https://docs.etherfuse.com/v2/api-reference/orders/get-order.md):
   * A self-held wallet gets `claimInstructions.preparedTransaction`. Its `transaction` adds the trustline and claims the balance in one step.
   * An [embedded wallet](https://docs.etherfuse.com/v2/guides/embedded-wallets.md) gets the order's `authorization` with `reason` `claim`, which holds the `message` for the key holder to sign.

5. **Sign and submit.** A self-held wallet signs `preparedTransaction.transaction` and submits it before `expiresAt`. For an embedded wallet, the key holder signs `authorization.method.message` and you send the signature to [POST /organization/\{organizationId}/order/\{id}/authorization](https://docs.etherfuse.com/v2/api-reference/orders/submit-authorization.md); see [Order authorization](https://docs.etherfuse.com/v2/guides/embedded-wallets.md#3-approve-the-authorization). After the claim lands, the wallet holds the trustline and the asset, and the order moves to `completed`.

```javascript theme={null}
// Using the Stellar SDK. Ask for a claim, then read it from the order.
const orderUrl = `https://api.sand.etherfuse.com/organization/${organizationId}/order/${orderId}`;
await fetch(`${orderUrl}/claim`, { method: "POST", headers: { Authorization: apiKey } });
// Wait for the orderUpdated webhook, or poll the order.
const order = await (await fetch(orderUrl, { headers: { Authorization: apiKey } })).json();
const prepared = order.claimInstructions?.preparedTransaction;
if (prepared) {
  const tx = TransactionBuilder.fromXDR(prepared.transaction, networkPassphrase);
  tx.sign(userKeypair);
  await server.submitTransaction(tx);
}
```

> [!NOTE]
>
> If the wallet already has an account and the trustline, we deliver directly. The order goes from `funded` to `completed`, and `claimInstructions` stays null.

> [!WARNING]
>
> **The claim transaction goes stale.** It uses the wallet's current sequence number, so any other transaction the wallet submits first breaks it. Each call to the claim endpoint prepares a new one, so call it again right before the customer signs.


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