Business Collection

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:

ItemValue
CountryMX
Fiat currencyMXN
Payment methodSPEI
StablecoinUSDT
Payer typeIndividual

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, and tenant_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.completed and onramp.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

EnvironmentBase URLDescription
Testhttps://api.test.stabofx.comFor development and testing
Productionhttps://api.stabo.ioLive 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

ModuleEndpointDescription
Receive AccountPOST /openapi/v1/collections/on-ramp/receive-accountsOpen a receive account
Receive AccountGET /openapi/v1/collections/on-ramp/receive-accounts/{payerId}Retrieve a receive account
QuoteGET /openapi/v1/collections/on-ramp/quoteGet a quote
OrderPOST /openapi/v1/collections/on-ramp/ordersCreate an order
OrderGET /openapi/v1/collections/on-ramp/orders/{orderId}Retrieve an order
WebhookHTTPS URL configured by the merchantReceive 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:

StatusDescriptionAction
PENDINGOpeningCall the Retrieve a receive account endpoint until the status changes to ACTIVE or FAILED
ACTIVEOpenYou can create orders
FAILEDOpening failedContact Stabo with the payerId and traceId

Notes:

  • This endpoint is idempotent. Repeated calls with the same payerId return the current status of the same receive account and do not open another account. Idempotency-Key is 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 payerId that 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 fiatAmount MXN to paymentInfo.clabe before expiresAt.
  • 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 fiatAmount and cryptoAmount.
  • When you create an order by fiatAmount, the provider confirms the USDT amount to be credited. The credited amount is the cryptoAmount in the response. Before the provider confirms, cryptoAmount can be empty.
  • An empty paymentInfo means the provider has not yet confirmed the collection. Retry the order creation with the same Idempotency-Key, or retrieve the order later.
  • If the provider rejects the request, the order is set to FAILED. A replay with the same Idempotency-Key returns that FAILED order.
  • 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

StatusDescriptionFinal status
AWAITING_PAYMENTWaiting for the payer to pay, or paid and waiting to be creditedNo
COMPLETEDCompleted. USDT has been credited to the merchant walletYes
FAILEDFailed, canceled, or expiredYes

Payers and receive accounts

  • Each payer has one dedicated receiving CLABE. The merchant identifies the payer by payerId.
  • The same payerId under 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-Key already 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-Key never expires and cannot be reused after any period.
  • Use a new Idempotency-Key for each new order.
  • Retry with the same Idempotency-Key in these cases: request timeout, network interruption, or a "receive account being opened" response.
  • Create a new order with a new Idempotency-Key in this case: the order is FAILED.

For the general rules, see Conventions.

Data format differences

On-Ramp amount precision differs from other products.

TypeFormatExample
AmountDecimal string. MXN has 2 decimal places (rounded half up). USDT has 6 decimal places (truncated)"954.77", "50.000000"
Exchange rateDecimal string. The MXN amount for 1 USDT"18.8253"
TimeISO 8601 with a time zone offset. Different fields can use different offsets (such as Z or +08:00). Parse each value by its offset2026-11-01T10:15:00+08:00
CountryISO 3166-1 alpha-2MX

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 statusScenario
200Success
400Request 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
401Token is invalid or expired
403Access denied
500System 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.

ErrorerrorCodemessageAction
Feature not enabledonramp.disabledOn-ramp is not enabledContact Stabo to enable it
Invalid requestonramp.request.invalidThe on-ramp request is missing required fields or has an invalid formatCheck the request parameters. See the common causes below
Missing idempotency keyerror.idempotent.key.missingIdempotency-Key header [Idempotency-Key] is requiredAdd the Idempotency-Key header to the order creation request
Receive account being openedonramp.receive_account.pendingThe payer receive account is being opened. Please retry later with the same Idempotency-KeyThe 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 foundonramp.receive_account.not_foundThe payer receive account does not existCall the Open a receive account endpoint first
Receive account opening failedonramp.receive_account.failedThe payer receive account could not be opened. Please contact operationsContact Stabo
Provider rejectedonramp.provider.rejectedThe 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 foundonramp.order.not_foundOn-ramp order not foundCheck the orderId
Other errorsAny 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, or cryptoCurrency is not MX, MXN, or USDT.
  • A quote request has no fiatAmount, or also passes cryptoAmount.
  • An order creation request passes both fiatAmount and cryptoAmount, or neither.
  • The order's Idempotency-Key is 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 typeTrigger
onramp.order.completedThe order is completed and USDT has been credited to the merchant wallet. The order status is COMPLETED
onramp.order.failedThe 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 format t=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, failureReason is the failure reason and completedAt is empty.
  • If an order fails before the provider confirms it, fiatAmount, cryptoAmount, price, and paymentInfo can be empty.
  • If the payer still completes the payment after the order is canceled or expired, the order changes to COMPLETED. The merchant receives onramp.order.failed and then onramp.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.

On this page