Integration Guides

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.

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)

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:

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:

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:

{
  "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.

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:

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:

{
  "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:

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:

{
  "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
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:

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:

[]

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

Response — a limit is exceeded:

[
  {
    "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)"
  }
]
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:

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:

{
  "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:

FieldValuePurpose
source.deposit_bank_info.ibanLT603250000000000001Stablegate's bank account where the customer sends EUR
source.deposit_bank_info.holder_nameSTGG AGPayee name the customer enters as the recipient of the transfer
referenceSG-20250115-ABCDEFMust be included as the payment reference in the SEPA transfer
valid_until2025-01-15T11:45:00.000ZDeadline for the deposit

Step 6: Customer Sends EUR via SEPA

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

DetailValue
Recipient IBANLT603250000000000001
Recipient NameSTGG AG — the holder_name from the order response
Bank NameStablegate UAB — the bank_name field, the bank that holds the account
Amount1,000.00 EUR
Payment ReferenceSG-20250115-ABCDEF
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:

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

{
  "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:

ItemValue
Customer sent1,000.00 EUR via SEPA
Fee15.00 EUR
Customer received978.50 USDC at 0xDC1d...41bB2
Statusdone

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:

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

Ready to integrate?

Tell us about your use case and we will enable Partner API access for your account.
Copyright © 2026