On-Ramp
Buy USDT with Mexican pesos (MXN). Receive account opening and queries, quotes, order creation and queries, order statuses, and Webhook events. For endpoint details, see the API Reference.
Overview
The On-Ramp API lets a merchant buy USDT with Mexican pesos (MXN) on behalf of a payer. The payer pays by SPEI transfer. When the order completes, USDT is credited to the merchant's Stabo wallet balance.
Capabilities in this release:
- Receive accounts: Open a dedicated CLABE for a payer and check its opening status
- Quote: Get a reference price by MXN amount
- Create an order: Create an order by MXN or USDT amount. The response returns the payer's dedicated CLABE and payment instructions
- Retrieve an order: Check the order status
- Order notifications: When an order completes or fails, Stabo pushes the order through a Webhook
Scope of this release:
| Item | Value |
|---|---|
| Country | MX |
| Fiat currency | MXN |
| Payment method | SPEI |
| Stablecoin | USDT |
| Payer type | Individual |
API fields may change. Changes are recorded in the Changelog.
Version: v0.1
Prerequisites
- Create API keys: In the Dashboard, go to Integrations → Keys and add a key to get
client_id,client_secret, andtenant_id. See Quickstart - Enable the feature: Contact your Stabo account manager to confirm that On-Ramp is enabled for you. If it is not enabled, the API returns
On-ramp is not enabled - Configure the IP allowlist: Use Edit IP Allowlist on the key to enter the egress IP addresses of your servers
- Configure Webhooks (recommended): In the Dashboard, go to Integrations → Webhooks, add an HTTPS endpoint, and subscribe to
onramp.order.completedandonramp.order.failed
For authentication, request headers, and common data formats, see Quickstart, Authentication, and Conventions. On-Ramp endpoints do not require X-Channel-Provider. The merchant identity comes from the token. The request body does not accept merchant or tenant fields.
Environments
| Environment | Base URL | Description |
|---|---|---|
| Test | https://api.test.stabofx.com | For development and testing |
| Production | https://api.stabo.io | Live production environment |
Integration flow
Open a receive account
When a payer registers or is linked, call the Open a receive account endpoint. Use the Retrieve a receive account endpoint to confirm that the status is ACTIVE.
Get a quote
Call the quote endpoint and show the reference price to the payer. A quote does not lock the price. The final price is the one in the order creation response.
Create an order
Call the order creation endpoint to get paymentInfo.clabe and the amount due, fiatAmount.
Payer transfers funds
Before expiresAt, the payer transfers fiatAmount MXN to the clabe.
Receive the result
Receive the Webhook notification, or poll the order query endpoint. When the status changes to COMPLETED, cryptoAmount USDT has been credited to the merchant wallet.
API overview
| Module | Endpoint | Description |
|---|---|---|
| Receive Account | POST /openapi/v1/collections/on-ramp/receive-accounts | Open a receive account |
| Receive Account | GET /openapi/v1/collections/on-ramp/receive-accounts/{payerId} | Retrieve a receive account |
| Quote | GET /openapi/v1/collections/on-ramp/quote | Get a quote |
| Order | POST /openapi/v1/collections/on-ramp/orders | Create an order |
| Order | GET /openapi/v1/collections/on-ramp/orders/{orderId} | Retrieve an order |
| Webhook | HTTPS URL configured by the merchant | Receive order completed and failed events |
For the parameters, responses, and interactive playground of each endpoint, see the API Reference.
Endpoint details
Open a receive account
Open a dedicated receiving CLABE for a payer. Opening takes a few minutes. Call this endpoint when the payer registers or is linked. Do not wait until payment time to open the account.
curl -X POST "https://api.test.stabofx.com/openapi/v1/collections/on-ramp/receive-accounts" \
-H "Authorization: Bearer {accessToken}" \
-H "Content-Type: application/json" \
-d '{
"payerId": "payer-0001",
"fiatCurrency": "MXN"
}'Response example:
{
"code": 200,
"message": "OK",
"traceId": "trace-uuid-string",
"data": {
"payerId": "payer-0001",
"fiatCurrency": "MXN",
"paymentMethod": "SPEI",
"status": "PENDING",
"clabe": null
}
}Opening statuses:
| Status | Description | Action |
|---|---|---|
PENDING | Opening | Call the Retrieve a receive account endpoint until the status changes to ACTIVE or FAILED |
ACTIVE | Open | You can create orders |
FAILED | Opening failed | Contact Stabo with the payerId and traceId |
Notes:
- This endpoint is idempotent. Repeated calls with the same
payerIdreturn the current status of the same receive account and do not open another account.Idempotency-Keyis not required. - Opening an account does not require payer KYC. You submit KYC when you create an order.
- This release does not push notifications for opening results. Use the Retrieve a receive account endpoint, and wait at least 1 minute between requests.
- The receive account does not expire. All future orders for the same payer use this CLABE.
Retrieve a receive account
Retrieve the opening status and CLABE of a payer's receive account. payerId is a path parameter. fiatCurrency is a query parameter, fixed to MXN.
curl -X GET "https://api.test.stabofx.com/openapi/v1/collections/on-ramp/receive-accounts/payer-0001?fiatCurrency=MXN" \
-H "Authorization: Bearer {accessToken}"The response structure is the same as the Open a receive account response. Response example when the account is open:
{
"code": 200,
"message": "OK",
"traceId": "trace-uuid-string",
"data": {
"payerId": "payer-0001",
"fiatCurrency": "MXN",
"paymentMethod": "SPEI",
"status": "ACTIVE",
"clabe": "706180000000000001"
}
}Notes:
- You can only retrieve payers of your own merchant account. A
payerIdthat has never been opened returns "receive account not found".
Get a quote
Quotes support MXN amounts only. You can create an order by MXN (fiatAmount) or USDT (cryptoAmount) amount.
curl -X GET "https://api.test.stabofx.com/openapi/v1/collections/on-ramp/quote?country=MX&fiatCurrency=MXN&cryptoCurrency=USDT&fiatAmount=954.77" \
-H "Authorization: Bearer {accessToken}"A quote does not generate a quote ID. You do not need to pass quote information when you create an order.
Create an order
Idempotency-Key is required, up to 128 characters. We recommend a UUID. Pass exactly one of fiatAmount and cryptoAmount. All payer information in the examples is fictitious.
curl -X POST "https://api.test.stabofx.com/openapi/v1/collections/on-ramp/orders" \
-H "Authorization: Bearer {accessToken}" \
-H "Idempotency-Key: 7f9c2a1e-0b6d-4c2f-9d7e-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{
"referenceNo": "merchant-order-0001",
"country": "MX",
"fiatCurrency": "MXN",
"cryptoCurrency": "USDT",
"cryptoAmount": "50.000000",
"payerId": "payer-0001",
"payer": {
"name": "Juan Perez Lopez",
"email": "juan@example.com",
"phone": "+5215512345678",
"country": "MX",
"address": "Av. Reforma 1, Ciudad de Mexico",
"dateOfBirth": "1990-01-31",
"idType": "NATIONAL_ID",
"idNumber": "XEXX010101HNEXXXA4"
},
"payerAccount": {
"accountType": "BANK",
"accountNumber": "012180001234567891",
"accountName": "Juan Perez Lopez"
},
"remark": "optional"
}'Response example:
{
"code": 200,
"message": "OK",
"traceId": "trace-uuid-string",
"data": {
"orderId": "bco_8f3k2m9q1x7z4t6w",
"referenceNo": "merchant-order-0001",
"status": "AWAITING_PAYMENT",
"country": "MX",
"fiatCurrency": "MXN",
"fiatAmount": "954.77",
"fiatFee": "13.50",
"cryptoCurrency": "USDT",
"cryptoAmount": "50.000000",
"price": "18.8253",
"paymentInfo": {
"method": "SPEI",
"clabe": "706180000000000001",
"reference": "REF123456"
},
"payer": {
"name": "Juan Perez Lopez"
},
"expiresAt": "2026-11-01T06:15:00Z",
"createdAt": "2026-11-01T10:15:00+08:00",
"completedAt": null,
"failureReason": null
}
}Notes:
- The payer must transfer
fiatAmountMXN topaymentInfo.clabebeforeexpiresAt. - The provider sets the order price in real time. It can differ from the quote. The order creation response is authoritative.
- Pass exactly one of
fiatAmountandcryptoAmount. - When you create an order by
fiatAmount, the provider confirms the USDT amount to be credited. The credited amount is thecryptoAmountin the response. Before the provider confirms,cryptoAmountcan be empty. - An empty
paymentInfomeans the provider has not yet confirmed the collection. Retry the order creation with the sameIdempotency-Key, or retrieve the order later. - If the provider rejects the request, the order is set to
FAILED. A replay with the sameIdempotency-Keyreturns thatFAILEDorder. - Payer KYC is only forwarded to the provider. Stabo does not store it, and does not return the ID number or contact details in responses.
- The payer's receive account must already be open. See Open a receive account. If it is not open, order creation returns "receive account being opened" and does not create an order.
Retrieve an order
curl -X GET "https://api.test.stabofx.com/openapi/v1/collections/on-ramp/orders/bco_8f3k2m9q1x7z4t6w" \
-H "Authorization: Bearer {accessToken}"The response structure is the same as the order creation response.
Notes:
- You can only retrieve On-Ramp orders of your own merchant account. Other orders return "order not found".
- Use Webhooks as the primary channel and order queries as a fallback. If you poll, wait at least 30 seconds between requests, and stop when the order reaches a final status.
- If a Webhook and an order query disagree, the order query result is authoritative.
Order statuses
| Status | Description | Final status |
|---|---|---|
AWAITING_PAYMENT | Waiting for the payer to pay, or paid and waiting to be credited | No |
COMPLETED | Completed. USDT has been credited to the merchant wallet | Yes |
FAILED | Failed, canceled, or expired | Yes |
Payers and receive accounts
- Each payer has one dedicated receiving CLABE. The merchant identifies the payer by
payerId. - The same
payerIdunder the same merchant always maps to the same CLABE. - The merchant generates
payerId. It is 1 to 64 characters, and allows only letters, digits,_, and-. - Do not use personal information such as email addresses, phone numbers, or ID numbers as
payerId. - Call the Open a receive account endpoint in advance, when the payer registers or is linked. Opening takes a few minutes. You can create orders only after the status changes to
ACTIVE. - If the receive account is not open when you create an order, Stabo starts opening it automatically. Order creation returns "receive account being opened" and does not create an order. This is only a fallback. Do not rely on it, or the payer has to wait for the account to open at payment time.
Idempotency
The order creation endpoint requires Idempotency-Key, up to 128 characters. We recommend a UUID.
- Within the same merchant, if the
Idempotency-Keyalready exists, the original order is returned and no new order is created. - A replay returns the order from the first request. The new request body is not used.
- An
Idempotency-Keynever expires and cannot be reused after any period. - Use a new
Idempotency-Keyfor each new order. - Retry with the same
Idempotency-Keyin these cases: request timeout, network interruption, or a "receive account being opened" response. - Create a new order with a new
Idempotency-Keyin this case: the order isFAILED.
For the general rules, see Conventions.
Data format differences
On-Ramp amount precision differs from other products.
| Type | Format | Example |
|---|---|---|
| Amount | Decimal string. MXN has 2 decimal places (rounded half up). USDT has 6 decimal places (truncated) | "954.77", "50.000000" |
| Exchange rate | Decimal string. The MXN amount for 1 USDT | "18.8253" |
| Time | ISO 8601 with a time zone offset. Different fields can use different offsets (such as Z or +08:00). Parse each value by its offset | 2026-11-01T10:15:00+08:00 |
| Country | ISO 3166-1 alpha-2 | MX |
Amount fields in requests accept numbers or strings. We recommend strings to avoid floating-point precision issues.
Business errors
For the error response format, see Errors. Use the HTTP status code to determine success. The code in the body is not always the same as the HTTP status code.
Business errors return HTTP 400 with code 500 in the response body. Response example:
{
"code": 500,
"message": "The payer receive account is being opened. Please retry later with the same Idempotency-Key",
"traceId": "trace-uuid-string",
"data": {
"errorCode": "onramp.receive_account.pending"
}
}| HTTP status | Scenario |
|---|---|
| 200 | Success |
| 400 | Request failed, including validation failures and business errors. The response body code is 500. Validation failure: message lists the invalid fields, for example cryptoAmount must be greater than 0, and data is empty. Business error: data.errorCode is the error code. See the table below |
| 401 | Token is invalid or expired |
| 403 | Access denied |
| 500 | System error. Keep the traceId and contact Stabo. For order creation, you can retry with the same Idempotency-Key |
When you contact Stabo to troubleshoot, provide the traceId.
Use data.errorCode to identify business errors. message is for display and troubleshooting only. Its content can change. Do not use it in program logic.
| Error | errorCode | message | Action |
|---|---|---|---|
| Feature not enabled | onramp.disabled | On-ramp is not enabled | Contact Stabo to enable it |
| Invalid request | onramp.request.invalid | The on-ramp request is missing required fields or has an invalid format | Check the request parameters. See the common causes below |
| Missing idempotency key | error.idempotent.key.missing | Idempotency-Key header [Idempotency-Key] is required | Add the Idempotency-Key header to the order creation request |
| Receive account being opened | onramp.receive_account.pending | The payer receive account is being opened. Please retry later with the same Idempotency-Key | The receive account is not open yet. Call the Retrieve a receive account endpoint. After the status changes to ACTIVE, retry the order creation with the same Idempotency-Key |
| Receive account not found | onramp.receive_account.not_found | The payer receive account does not exist | Call the Open a receive account endpoint first |
| Receive account opening failed | onramp.receive_account.failed | The payer receive account could not be opened. Please contact operations | Contact Stabo |
| Provider rejected | onramp.provider.rejected | The provider rejected the request: {reason} | The order is set to FAILED. Check the payer information, then create a new order with a new Idempotency-Key |
| Order not found | onramp.order.not_found | On-ramp order not found | Check the orderId |
| Other errors | Any other value | — | Keep the traceId and contact Stabo. For order creation, you can retry with the same Idempotency-Key |
Common causes of "invalid request":
country,fiatCurrency, orcryptoCurrencyis notMX,MXN, orUSDT.- A quote request has no
fiatAmount, or also passescryptoAmount. - An order creation request passes both
fiatAmountandcryptoAmount, or neither. - The order's
Idempotency-Keyis already used by another collection request of the same merchant.
Webhook events
When an order reaches a final status, Stabo pushes an event to the Webhook URL configured by the merchant.
Event types
| Event type | Trigger |
|---|---|
onramp.order.completed | The order is completed and USDT has been credited to the merchant wallet. The order status is COMPLETED |
onramp.order.failed | The order failed, was canceled, or the payment expired. The order status is FAILED |
Delivery
Delivery, request headers, and signature verification are the same as Webhooks for other products. See Webhooks.
- Method: HTTPS POST,
application/json. - Headers:
Webhook-Event-Id(unique event ID),Webhook-Event-Type(event type),Webhook-Signature(signature, in the formatt=timestamp,v1=signature). - Signature: HMAC-SHA256. The string to sign is
{Webhook-Event-Id}.{t}.{raw request body}. The merchant must verify the signature. - Retries: If the response is not 2xx, Stabo retries after 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours.
- Timeout: 30 seconds. We recommend returning 2xx first and processing asynchronously.
Event object
data.object is the order object. Its fields are the same as data in the order creation response.
{
"id": "evt_1234567890",
"object": "event",
"type": "onramp.order.completed",
"created": 1762151528,
"data": {
"object": {
"orderId": "bco_8f3k2m9q1x7z4t6w",
"referenceNo": "merchant-order-0001",
"status": "COMPLETED",
"country": "MX",
"fiatCurrency": "MXN",
"fiatAmount": "954.77",
"fiatFee": "13.50",
"cryptoCurrency": "USDT",
"cryptoAmount": "50.000000",
"price": "18.8253",
"paymentInfo": {
"method": "SPEI",
"clabe": "706180000000000001",
"reference": "REF123456"
},
"payer": {
"name": "Juan Perez Lopez"
},
"expiresAt": "2026-11-01T06:15:00Z",
"createdAt": "2026-11-01T10:15:00+08:00",
"completedAt": "2026-11-01T10:32:08+08:00",
"failureReason": null
}
}
}Notes
- Deduplicate by
Webhook-Event-Id. The same event can be delivered more than once because of retries. - In
onramp.order.failed,failureReasonis the failure reason andcompletedAtis empty. - If an order fails before the provider confirms it,
fiatAmount,cryptoAmount,price, andpaymentInfocan be empty. - If the payer still completes the payment after the order is canceled or expired, the order changes to
COMPLETED. The merchant receivesonramp.order.failedand thenonramp.order.completed. Rely on the latest event and the order query result.
FAQ
Errors and support
For general error handling, see Errors. For other questions, see the FAQ. For help, contact your Stabo account manager or technical support, and provide the traceId.
Business Collection
Collect in local fiat currency and receive funds as stablecoins in your Stabo wallet. On-Ramp is currently available.
Payment product guide
Balance inquiry, transfers, transaction inquiry, SWIFT messages, virtual accounts, and credit notifications. See the API reference for endpoint details.