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

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

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

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

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

      Each chain has its own setup (trustlines, gas). See the chain guides: [Solana](/v2/guides/chains/solana), [Stellar](/v2/guides/chains/stellar), [Base](/v2/guides/chains/base), [Polygon](/v2/guides/chains/polygon), [Monad](/v2/guides/chains/monad), [Avalanche](/v2/guides/chains/avalanche).
    </Expandable>
  </Step>

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

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

      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}](/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. Use the dummy RFC `XAXX010101000` in sandbox.
      </Note>
    </Expandable>
  </Step>

  <Step title="Create a quote: POST /organization/{organizationId}/quote">
    <Expandable title="Details">
      Quote from the bank account to the wallet. Both accounts must belong to the organization in the route. 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](/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": "<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"
          }'
        ```
      </CodeGroup>

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

      <Info>
        In sandbox, onramp quotes are limited to a `sourceAmount` of 500. Larger amounts return `400` `sandbox-limit-exceeded`.
      </Info>
    </Expandable>
  </Step>

  <Step title="Create the order: POST /organization/{organizationId}/order">
    <Expandable title="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](/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>", "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" }'
        ```
      </CodeGroup>

      `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](/v2/idempotency).

      `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`. `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="Simulate the fiat deposit: POST /organization/{organizationId}/order/{id}/fiat_received">
    <Expandable title="Details">
      In production, the customer pays the funding destination before `fundingDeadline`. In sandbox, simulate the deposit with [Simulate fiat received](/v2/api-reference/sandbox/simulate-fiat-received). 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.

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

      <Warning>
        **The customer must send the exact order amount.** Pay the `source.amount` from the order to the funding destination.
      </Warning>
    </Expandable>
  </Step>

  <Step title="Track the order: GET /organization/{organizationId}/order/{id}">
    <Expandable title="Details">
      Read the order, or subscribe to `orderUpdated` with [POST /webhook\_subscription](/v2/api-reference/webhook-subscriptions/create-webhook-subscription). Each [`orderUpdated`](/v2/api-reference/webhook-events/orderupdated) event carries the full order. See [Get order](/v2/api-reference/orders/get-order) for the response schema.

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

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

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

<Expandable title="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, so a stablebond onramp to another chain can include a `bridge` fee line. For an `avalanche` wallet, read the available assets from [GET /supported\_assets?chain=avalanche](/v2/api-reference/assets/list-supported-assets).
</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 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](/v2/errors) for every problem type.
</Expandable>

***

## 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](/v2/api-reference/orders/prepare-claim) with no body. The response depends on the wallet:
   * `stellarTransaction`: a self-held wallet signs and submits `xdr`. The transaction adds the trustline and claims the balance in one step.
   * `authorization`: an [embedded wallet](/v2/guides/embedded-wallets) gets an `authorizationId`. The order's `authorization` with that id holds the `message` for the key holder to sign.

5. **Sign and submit.** A self-held wallet submits the signed `xdr`. For an embedded wallet, the key holder signs `authorization.method.message` and you send the signature to [POST /organization/\{organizationId}/order/\{id}/authorization](/v2/api-reference/orders/submit-authorization); see [Order authorization](/v2/guides/embedded-wallets#order-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
const res = await fetch(
  `https://api.sand.etherfuse.com/organization/${organizationId}/order/${orderId}/claim`,
  { method: "POST", headers: { Authorization: apiKey } }
);
const claim = await res.json();
if (claim.kind === "stellarTransaction") {
  const tx = TransactionBuilder.fromXDR(claim.xdr, networkPassphrase);
  tx.sign(userKeypair);
  await server.submitTransaction(tx);
}
```

<Info>
  If the wallet already has an account and the trustline, we deliver directly. The order goes from `funded` to `completed`, and `claimInstructions` stays null.
</Info>

<Warning>
  **The claim transaction goes stale.** It uses the wallet's current sequence number, so any other transaction the wallet submits first breaks it. We build a new transaction on every call and never store one, so call the endpoint again right before the customer signs.
</Warning>


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