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
- Register wallet — register the customer's Ethereum wallet.
- Check route — verify SEPA/EUR → Ethereum/USDC is available.
- Check fees — calculate the fee for 1,000 EUR.
- Check limits — confirm the amount is within limits.
- Create order — submit the on-ramp order.
- Customer deposit — the customer sends EUR via SEPA transfer.
- Monitor status — poll until the order reaches a terminal state.
- 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.
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
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)"
}
]
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:
| Field | Value | Purpose |
|---|---|---|
source.deposit_bank_info.iban | LT603250000000000001 | Stablegate's bank account where the customer sends EUR |
source.deposit_bank_info.holder_name | STGG AG | Payee name the customer enters as the recipient of the transfer |
reference | SG-20250115-ABCDEF | Must be included as the payment reference in the SEPA transfer |
valid_until | 2025-01-15T11:45:00.000Z | Deadline for the deposit |
Step 6: Customer Sends EUR via SEPA
Instruct your customer to initiate a SEPA bank transfer with the following details:
| Detail | Value |
|---|---|
| Recipient IBAN | LT603250000000000001 |
| Recipient Name | STGG AG — the holder_name from the order response |
| Bank Name | Stablegate UAB — the bank_name field, the bank that holds the account |
| Amount | 1,000.00 EUR |
| Payment Reference | SG-20250115-ABCDEF |
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:
new— Order created, waiting for deposit.source.deposit_statusstaysnulluntil the transfer arrivesnewwithsource.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 creditedprocessing— EUR deposit credited, exchange in progressdone— 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:
| Item | Value |
|---|---|
| Customer sent | 1,000.00 EUR via SEPA |
| Fee | 15.00 EUR |
| Customer received | 978.50 USDC at 0xDC1d...41bB2 |
| Status | done |
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?
Integration Checklist
The path from the first request to go-live, in four phases. Each item is a question to answer before moving on, with a pointer to the page that has the details. Coverage says what the API supports; this page says in what order to use it.
Off-Ramp Guide
This guide walks you through a complete off-ramp flow — converting USDT to EUR via the Stablegate API. By the end, your customer will have EUR delivered to their bank account.