---
title: "Guide: On-Ramp (Fiat to Crypto)"
description: "This guide walks you through a complete on-ramp flow — converting EUR to USDC via the Stablegate API. By the end, your customer will have USDC delivered to their Ethereum wallet."
canonical_url: "https://docs.stablegate.com/docs/guide/integration-guides/onramp"
---
# Guide: On-Ramp (Fiat to Crypto)

> This guide walks you through a complete on-ramp flow — converting EUR to USDC via the Stablegate API. By the end, your customer will have USDC delivered to their Ethereum wallet.

This guide walks you through a complete on-ramp flow — converting EUR to USDC via the Stablegate API. By the end, your customer will have USDC delivered to their Ethereum wallet.

**Scenario:** Your customer wants to buy 1,000 EUR worth of USDC, receiving the tokens at their Ethereum wallet address.

## Prerequisites

Before you begin, make sure you have:

- [ ] API key configured
- [ ] Customer UUID (created via the [Customers API](https://docs.stablegate.com/docs/guide/customers))

## Flow Overview

1. **Register wallet** — register the customer's Ethereum wallet.
2. **Check route** — verify SEPA/EUR → Ethereum/USDC is available.
3. **Check fees** — calculate the fee for 1,000 EUR.
4. **Check limits** — confirm the amount is within limits.
5. **Create order** — submit the on-ramp order.
6. **Customer deposit** — the customer sends EUR via SEPA transfer.
7. **Monitor status** — poll until the order reaches a terminal state.
8. **Complete** — USDC is delivered to the customer's wallet.

---

## Step 1: Register the Customer's Ethereum Wallet

Before creating an on-ramp order, register the wallet that will receive the crypto. For Ethereum wallets, ownership verification is required.

**Construct the ownership proof:**

```javascript
import { ethers } from "ethers";

function stringToHex(str) {
  let hex = "";
  for (let i = 0; i < str.length; i++) {
    hex += str.charCodeAt(i).toString(16);
  }
  return `0x${hex}`;
}

// Customer's wallet
const wallet = new ethers.Wallet("CUSTOMER_PRIVATE_KEY");

const ts = new Date().toISOString();
const message = `I confirm the ownership of address ${wallet.address}. Signature time: ${ts}.`;

const wallet_signature = await wallet.signMessage(message);
const wallet_message = stringToHex(message);
```

**Request:**

::code-switcher{body="{"rail":"ethereum","address":"0xDC1dCb46fC96C46c4F294866AFCA81874041bB2","name":"Customer Main Wallet","wallet_message":"0x4920636f6e6669726d20746865206f776e657273686970206f662061646472657373203078444331644362343666433936433436633446323934383636364146434138313837343034316242322e205369676e61747572652074696d653a20323032352d30312d31355431303a33303a30302e3030305a2e","wallet_signature":"0x861818884bbf61f8d83bc3052e6c513f939512852fc92067dde5845b392248c949812e44366158d91d73ac0e9588c56dfb1d59549c3cbad52cfe4884fdc15c361"}" method="POST" path="/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/wallets/"}
```bash
curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/wallets/ \
  --request POST \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "rail": "ethereum",
  "address": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2",
  "name": "Customer Main Wallet",
  "wallet_message": "0x4920636f6e6669726d20746865206f776e657273686970206f662061646472657373203078444331644362343666433936433436633446323934383636364146434138313837343034316242322e205369676e61747572652074696d653a20323032352d30312d31355431303a33303a30302e3030305a2e",
  "wallet_signature": "0x861818884bbf61f8d83bc3052e6c513f939512852fc92067dde5845b392248c949812e44366158d91d73ac0e9588c56dfb1d59549c3cbad52cfe4884fdc15c361"
}'
```
::

**Response:**

```json
{
  "uuid": "wa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "name": "Customer Main Wallet",
  "rail": "ethereum",
  "address": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2",
  "created_at": "2025-01-15T10:30:00.000Z",
  "updated_at": "2025-01-15T10:30:00.000Z"
}
```

Save the `uuid` — you will use it when creating the order.

:note[If the wallet is already registered, skip this step and use the existing wallet UUID.]---

## Step 2: Check Exchange Route Availability

Verify that the SEPA EUR → Ethereum USDC route is currently available.

**Request:**

::code-switcher{body="{"source":{"rail":"sepa","asset_code":"EUR"},"destination":{"rail":"ethereum","asset_code":"USDC"}}" method="POST" path="/v1/exchange-routes/check/"}
```bash
curl https://api.stablegate.com/v1/exchange-routes/check/ \
  --request POST \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "source": {
    "rail": "sepa",
    "asset_code": "EUR"
  },
  "destination": {
    "rail": "ethereum",
    "asset_code": "USDC"
  }
}'
```
::

**Response:**

```json
{
  "available": true,
  "route": {
    "source": {
      "rail": "sepa",
      "asset": {
        "code": "EUR",
        "fraction_digits": 2
      }
    },
    "destination": {
      "rail": "ethereum",
      "asset": {
        "code": "USDC",
        "fraction_digits": 6
      }
    }
  }
}
```

The route is available. If `available` were `false`, you would need to inform the customer that this conversion path is temporarily unavailable.

---

## Step 3: Calculate Fees

Check how much the customer will receive for 1,000 EUR after fees.

**Request:**

::code-switcher{body="{"source":{"rail":"sepa","asset":"EUR","amount":1000},"destination":{"rail":"ethereum","asset":"USDC"}}" method="POST" path="/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-fees/"}
```bash
curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-fees/ \
  --request POST \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "source": {
    "rail": "sepa",
    "asset": "EUR",
    "amount": 1000
  },
  "destination": {
    "rail": "ethereum",
    "asset": "USDC"
  }
}'
```
::

**Response:**

```json
{
  "source": {
    "rail": "sepa",
    "asset": "EUR",
    "amount": "1000.00"
  },
  "destination": {
    "rail": "ethereum",
    "asset": "USDC",
    "amount": "978.50"
  },
  "commission": {
    "asset": "EUR",
    "amount": "15.00"
  },
  "exchange_rate": "1.006700"
}
```

**Display to the customer:**

- Sends: **1,000.00 EUR**
- Fee: **15.00 EUR**
- Exchange rate: **1.0067** (EUR/USDC)
- Receives: **~978.50 USDC**

:note[The final amount may vary slightly due to exchange rate fluctuations between the fee check and order execution.]---

## Step 4: Verify Limits

Confirm the customer has sufficient limit capacity for this transaction.

**Request:**

::code-switcher{body="{"source":{"rail":"sepa","asset":"EUR","amount":1000},"destination":{"rail":"ethereum","asset":"USDC"}}" method="POST" path="/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-limits/"}
```bash
curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-limits/ \
  --request POST \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "source": {
    "rail": "sepa",
    "asset": "EUR",
    "amount": 1000
  },
  "destination": {
    "rail": "ethereum",
    "asset": "USDC"
  }
}'
```
::

**Response — the operation passes:**

```json
[]
```

The endpoint returns only the rules the operation breaks, so an empty array is the success case.

**Response — a limit is exceeded:**

```json
[
  {
    "limit_rule": {
      "uuid": "lr1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
      "name": "Lifetime turnover",
      "scope": "period",
      "period": "lifetime",
      "rail_types": ["bank"],
      "rail_codes": null,
      "operation_types": ["funding", "payout"],
      "asset_codes": null
    },
    "limit_amount": "15000.00",
    "used_amount": "14500.00",
    "available_amount": "500.00",
    "excess_amount": "500.00",
    "error_msg": "Lifetime turnover is exceeded by 500 EUR (the current limit is 15000 EUR)"
  }
]
```

::note
**Any element in the array is a refusal**, even when `excess_amount` is `"0.00"`: the minimum-amount rule reports itself that way, and the reason sits in `error_msg`. Show the customer `available_amount` and suggest a smaller transaction.
::

---

## Step 5: Create the On-Ramp Order

Submit the order using the wallet UUID from Step 1.

**Request:**

::code-switcher{body="{"source":{"rail":"sepa","asset":"EUR","amount":1000},"destination":{"rail":"ethereum","asset":"USDC","client_account_uuid":"wa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"}}" idempotency-key="order_c1d2e3f4_onramp_001" method="POST" path="/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/"}
```bash
curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/ \
  --request POST \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: order_c1d2e3f4_onramp_001' \
  --data '{
  "source": {
    "rail": "sepa",
    "asset": "EUR",
    "amount": 1000
  },
  "destination": {
    "rail": "ethereum",
    "asset": "USDC",
    "client_account_uuid": "wa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"
  }
}'
```
::

**Response:**

```json
{
  "uuid": "od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a",
  "reference": "SG-20250115-ABCDEF",
  "state": "new",
  "deposit_address": "",
  "source": {
    "rail": "sepa",
    "asset": "EUR",
    "amount": "1000.00",
    "deposit_bank_info": {
      "iban": "LT603250000000000001",
      "bank_name": "Stablegate UAB",
      "bank_code": "REVOLT21",
      "bank_country": "LT",
      "holder_name": "STGG AG",
      "bank_address": "Konstitucijos pr. 21B, 08130 Vilnius, Lithuania"
    },
    "transaction_hash": null,
    "deposit_status": null
  },
  "destination": {
    "rail": "ethereum",
    "asset": "USDC",
    "amount": "978.50",
    "client_account": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2"
  },
  "commission": {
    "asset": "EUR",
    "amount": "15.00"
  },
  "account": {
    "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
    "code": "TRD-001",
    "name": "Trading Account",
    "type": "trading"
  },
  "error_message": null,
  "created_at": "2025-01-15T10:45:00.000Z",
  "valid_until": "2025-01-15T11:45:00.000Z"
}
```

Save the order `uuid` for tracking. Take note of these critical fields:

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Value
    </th>
    
    <th>
      Purpose
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        source.deposit_bank_info.iban
      </code>
    </td>
    
    <td>
      <code>
        LT603250000000000001
      </code>
    </td>
    
    <td>
      Stablegate's bank account where the customer sends EUR
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        source.deposit_bank_info.holder_name
      </code>
    </td>
    
    <td>
      <code>
        STGG AG
      </code>
    </td>
    
    <td>
      Payee name the customer enters as the recipient of the transfer
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        reference
      </code>
    </td>
    
    <td>
      <code>
        SG-20250115-ABCDEF
      </code>
    </td>
    
    <td>
      Must be included as the payment reference in the SEPA transfer
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        valid_until
      </code>
    </td>
    
    <td>
      <code>
        2025-01-15T11:45:00.000Z
      </code>
    </td>
    
    <td>
      Deadline for the deposit
    </td>
  </tr>
</tbody>
</table>

---

## Step 6: Customer Sends EUR via SEPA

Instruct your customer to initiate a SEPA bank transfer with the following details:

<table>
<thead>
  <tr>
    <th>
      Detail
    </th>
    
    <th>
      Value
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Recipient IBAN
      </strong>
    </td>
    
    <td>
      <code>
        LT603250000000000001
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Recipient Name
      </strong>
    </td>
    
    <td>
      <code>
        STGG AG
      </code>
      
       — the <code>
        holder_name
      </code>
      
       from the order response
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Bank Name
      </strong>
    </td>
    
    <td>
      <code>
        Stablegate UAB
      </code>
      
       — the <code>
        bank_name
      </code>
      
       field, the bank that holds the account
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Amount
      </strong>
    </td>
    
    <td>
      <code>
        1,000.00 EUR
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Payment Reference
      </strong>
    </td>
    
    <td>
      <code>
        SG-20250115-ABCDEF
      </code>
    </td>
  </tr>
</tbody>
</table>

::warning
The payment reference must exactly match the `reference` from the order response. Without it, the deposit cannot be automatically matched to the order.
::

---

## Step 7: Monitor Order Status

After the customer initiates the SEPA transfer, poll the order status. SEPA transfers typically settle within 1 business day.

**Request:**

::code-switcher{method="GET" path="/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a/"}
```bash
curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a/ \
  --header 'X-Api-Key: YOUR_API_KEY'
```
::

**Status transitions you will observe:**

1. **new** — Order created, waiting for deposit. `source.deposit_status` stays `null` until the transfer arrives
2. **new** with `source.deposit_status: "received"` or `"confirmed"` — the transfer is on Stablegate's account and is being matched. `"on_hold"` means compliance holds it, `"rejected"` means it will not be credited
3. **processing** — EUR deposit credited, exchange in progress
4. **done** — Exchange complete, USDC sent to the customer's wallet

**Response (completed):**

```json
{
  "uuid": "od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a",
  "reference": "SG-20250115-ABCDEF",
  "state": "done",
  "deposit_address": "",
  "source": {
    "rail": "sepa",
    "asset": "EUR",
    "amount": "1000.00",
    "deposit_bank_info": {
      "iban": "LT603250000000000001",
      "bank_name": "Stablegate UAB",
      "bank_code": "REVOLT21",
      "bank_country": "LT",
      "holder_name": "STGG AG",
      "bank_address": "Konstitucijos pr. 21B, 08130 Vilnius, Lithuania"
    },
    "transaction_hash": null,
    "deposit_status": "credited"
  },
  "destination": {
    "rail": "ethereum",
    "asset": "USDC",
    "amount": "978.50",
    "client_account": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2"
  },
  "commission": {
    "asset": "EUR",
    "amount": "15.00"
  },
  "account": {
    "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
    "code": "TRD-001",
    "name": "Trading Account",
    "type": "trading"
  },
  "error_message": null,
  "created_at": "2025-01-15T10:45:00.000Z",
  "valid_until": null,
  "exchange_rate": "0.9785",
  "rate_valid_until": null
}
```

---

## Step 8: Order Complete

The order is in the `done` state. The customer's Ethereum wallet (`0xDC1dCb46...`) has received **978.50 USDC**.

**Summary:**

<table>
<thead>
  <tr>
    <th>
      Item
    </th>
    
    <th>
      Value
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Customer sent
      </strong>
    </td>
    
    <td>
      1,000.00 EUR via SEPA
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Fee
      </strong>
    </td>
    
    <td>
      15.00 EUR
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Customer received
      </strong>
    </td>
    
    <td>
      978.50 USDC at <code>
        0xDC1d...41bB2
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Status
      </strong>
    </td>
    
    <td>
      <code>
        done
      </code>
    </td>
  </tr>
</tbody>
</table>

---

## Error Handling

### Route unavailable

If the route check in Step 2 returns `available: false`, the conversion path is temporarily unavailable. Inform the customer and suggest trying again later.

### Limits exceeded

If the check in Step 4 returns a non-empty array, the transaction breaks a limit. Do not look at `excess_amount` alone: the minimum-amount rule reports itself with `excess_amount: "0.00"` and the reason in `error_msg`. Show `available_amount` and suggest a smaller transaction.

### Order failed

If the order reaches a `failed` or `rejected` state, check the `error_message` field for details.

A transfer that arrives with a different amount does not fail the order by itself: the payout is recalculated from the amount actually received, so an under- or overpayment changes the amount the customer gets. A transfer that arrives without the `reference` cannot be matched automatically and is handled by Stablegate support.

### Order expired

If the customer does not send the SEPA transfer before `valid_until`, the order may expire. Create a new order if needed.

### Order cancellation

To cancel an order before the deposit is received:

::code-switcher{method="POST" path="/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a/cancel/"}
```bash
curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a/cancel/ \
  --request POST \
  --header 'X-Api-Key: YOUR_API_KEY'
```
::

**Response:** `204 No Content`


## Sitemap

See the full [sitemap](https://docs.stablegate.com/sitemap.md) for all pages.
