Onramp API Reference
Base URLs: https://onramp.mainnet.rubin.trade (mainnet), https://onramp.testnet.rubin.trade (testnet). All customer endpoints live under the route /v4/sbp; Rubin assigns each customer a payment provider behind it (see Overview).
Auth column: public — no signature; signed — signed request for the customer address.
| Method | Path | Auth | Applies to |
|---|---|---|---|
| GET | /v4/sbp/rates | public / signed if ratesPerCustomer | all |
| GET | /v4/sbp/customer | signed | all |
| POST | /v4/sbp/customer | signed | identity: form |
| POST | /v4/sbp/customer/resend-otp | signed | identity: form |
| POST | /v4/sbp/customer/verify-phone | signed | identity: form |
| POST | /v4/sbp/customer/identify | signed | identity: form |
| POST | /v4/sbp/customer/identify/update | signed | identity: form |
| POST | /v4/sbp/customer/esia | signed | identity: form |
| POST | /v4/sbp/customer/kyc | signed | identity: hosted |
| POST | /v4/sbp/customer/contacts | signed | contacts: true |
| GET | /v4/sbp/limits | signed | all |
| POST | /v4/sbp/orders | signed | all |
| GET | /v4/sbp/orders | signed | all |
| GET | /v4/sbp/orders/{orderId} | signed | all |
An endpoint the customer's flow does not use answers 404 (after authentication and validation). Amounts are decimal strings; timestamps are ISO 8601.
Errors
Every error has the same shape:
{ "errors": [{ "code": "QUOTE_EXPIRED", "msg": "Quote expired", "retryAfterSeconds": 30 }] }code and retryAfterSeconds are present only where they apply. Request validation errors (400) are a list of { "value", "msg", "param", "location" } without code. Pauses are reported only in retryAfterSeconds — there is no Retry-After header.
| Code | HTTP | Meaning |
|---|---|---|
PHONE_ALREADY_REGISTERED | 409 | The phone number belongs to another customer |
SMS_RATE_LIMIT | 429 | Too many SMS or code attempts; wait retryAfterSeconds |
OTP_INVALID | 422 | Wrong SMS code |
OTP_EXPIRED | 422 | The SMS code expired; send a new one |
IDENTIFY_FAILED | 422 | Identity document or eID check failed |
NOT_VERIFIED | 422 | The customer is below level 3 |
CONTACTS_REQUIRED | 422 | Hosted flow: contacts are not submitted yet |
CONTACTS_CONFLICT | 422 | Hosted flow: contacts need manual review |
QUOTE_EXPIRED | 422 | The quote is stale or invalid; request a new one |
AMOUNT_BELOW_MIN | 422 | Below the minimum order |
AMOUNT_ABOVE_MAX | 422 | Above the maximum order or the on-chain per-deposit cap |
LIMIT_EXCEEDED | 422 | The customer's daily, weekly or monthly limit is used up |
PROVIDER_LIQUIDITY | 422 | Not enough liquidity for this amount (flow.poolLimited) |
CUSTOMER_NOT_FOUND | 404 | No customer for this address yet |
ORDER_NOT_FOUND | 404 | No such order |
PROVIDER_UNAVAILABLE | 502 / 429 | The payment service is down or throttling; on 429 wait retryAfterSeconds |
PROVIDER_ERROR | 502 / 422 | The payment provider rejected the request |
Provider errors carry a fixed msg per code. To investigate a specific failure, send us the address, the time and the response.
Authentication errors are listed on the Authentication page.
Rates
GET /v4/sbp/rates
Current rate for buying USDC with fiat (RUB). Public; sign it when the customer's flow.ratesPerCustomer is true.
{ "userRate": "81.250000", "currency": "RUB", "quoteSignature": "…", "expiresInSeconds": 165 }| Field | Type | Description |
|---|---|---|
userRate | string | RUB per 1 USDC, final for the customer (all fees included) |
currency | string | Fiat currency, RUB |
quoteSignature | string | Pass to POST /orders |
expiresInSeconds | number | Seconds until the quote expires |
Errors: NOT_VERIFIED, CUSTOMER_NOT_FOUND (personal rates only), PROVIDER_UNAVAILABLE, PROVIDER_ERROR.
Customer
GET /v4/sbp/customer
Verification state of the signing address and the steps it needs. The first call of an address assigns it a payment provider.
{
"flow": {
"identity": "form",
"phoneOtp": true,
"contacts": false,
"ratesPerCustomer": false,
"settlement": false,
"poolLimited": false
},
"exists": true,
"verificationLevel": 3,
"identified": true,
"phoneSet": true,
"phoneMasked": "+7***1234",
"identifyStatus": "success",
"activeOrderId": null
}| Field | Type | Description |
|---|---|---|
flow | object | Steps this customer needs — see Integration flow |
exists | boolean | false — verification not started yet (verificationLevel is 0) |
verificationLevel | number | See levels; orders need 3 |
identified | boolean | Identity confirmed |
phoneSet | boolean | A phone number is attached (only when exists is true) |
phoneMasked | string | null | Masked phone, e.g. +7***1234 |
identifyStatus | string | none, pending, success, failed |
activeOrderId | string | null | Latest open order (initiated, processing or awaiting_payment) |
kycStatus | string | Hosted flow (and form flow when reported): none, pending, approved, rejected, expired |
contactsStatus | string | Hosted flow: none, pending, ready, action_required, failed, review_required |
contactsActionUrl | string | null | Hosted flow: page to open when contactsStatus is action_required |
POST /v4/sbp/customer
Attaches a phone number and sends an SMS code. Form flow.
| Body field | Type | Required | Description |
|---|---|---|---|
phone | string | yes | +7 and 10 digits, up to 32 chars |
Response: { "verificationLevel": 1, "otpSent": true, "nextStep": "verify_phone" }.
Errors: PHONE_ALREADY_REGISTERED, SMS_RATE_LIMIT, PROVIDER_ERROR (SMS could not be sent).
POST /v4/sbp/customer/resend-otp
Sends the SMS code again. Body: { "phone": "+79991234567" }. Response: { "otpSent": true }. Errors: SMS_RATE_LIMIT.
POST /v4/sbp/customer/verify-phone
| Body field | Type | Required | Description |
|---|---|---|---|
otpCode | string | yes | Code from the SMS, 4 digits |
Response: { "verificationLevel": 2, "identified": false, "phoneSet": true }. A 200 with verificationLevel below 2 means the code was not accepted.
Errors: OTP_INVALID, OTP_EXPIRED, SMS_RATE_LIMIT, CUSTOMER_NOT_FOUND.
POST /v4/sbp/customer/identify
Identity-document check. POST …/customer/identify/update takes the same body to correct the details of a customer already at level 3.
| Body field | Type | Required | Description |
|---|---|---|---|
lastName | string | yes | Up to 128 chars |
firstName | string | yes | Up to 128 chars |
patronymic | string | no | Up to 128 chars |
birthDate | string | yes | YYYY-MM-DD |
persondocNumber | string | yes | Document series and number, 10 digits |
Response: { "verificationLevel": 3, "identified": true, "identifyStatus": "success" }. With identifyStatus: "pending" poll GET /customer.
Errors: IDENTIFY_FAILED, CUSTOMER_NOT_FOUND.
POST /v4/sbp/customer/esia
Starts a government eID (ESIA) login to raise the customer to level 4.
| Body field | Type | Required | Description |
|---|---|---|---|
redirectUrl | string | no | Where to return the user afterwards; HTTPS, allow-listed origin |
Response: { "redirectUrl": "https://…" } — send the user there. After they return, poll GET /customer until verificationLevel is 4.
Errors: 422 "ESIA is not enabled", 422 "redirectUrl is missing or not allowed", IDENTIFY_FAILED.
POST /v4/sbp/customer/kyc
Returns the hosted KYC page. Hosted flow.
| Body field | Type | Required | Description |
|---|---|---|---|
returnUrl | string | no | HTTPS page to return to; ignored if not allowed |
Response: { "url": "https://…", "status": "pending" }. url is null while the page is being prepared or when KYC is already approved.
POST /v4/sbp/customer/contacts
Hosted flow with flow.contacts.
| Body field | Type | Required | Description |
|---|---|---|---|
phone | string | yes | E.164, up to 32 chars |
email | string | yes | Up to 254 chars |
Response: { "status": "action_required", "actionUrl": "https://…" }. Open actionUrl on action_required; poll GET /customer on pending.
Errors: CONTACTS_CONFLICT.
Limits
GET /v4/sbp/limits
| Field | Type | Description |
|---|---|---|
currency | string | RUB |
remainingRubles | string | What the customer may still buy now |
remainingDailyRubles | string | null | Left for today |
remainingWeeklyRubles | string | null | Left for this week |
remainingMonthlyRubles | string | null | Left for this month |
dailyLimitRubles | string | null | Daily ceiling |
weeklyLimitRubles | string | null | Weekly ceiling |
monthlyLimitRubles | string | null | Monthly ceiling |
minOrderRubles | string | null | Minimum order |
maxOrderRubles | string | null | Maximum order |
availableUsdc | string | null | Available liquidity in USDC (when flow.poolLimited) |
verificationLevel | number | Current level |
null means that limit is not reported. Errors: CUSTOMER_NOT_FOUND.
Orders
POST /v4/sbp/orders
Creates an order and requests a payment link.
| Body field | Type | Required | Description |
|---|---|---|---|
amountCrypto | string | yes | USDC to credit, up to 6 decimals, e.g. "125.50" |
quoteSignature | string | yes | From GET /rates |
subaccountNumber | number | no | 0–128000; stored with the order. Funds are always credited to the main account |
Response: an order.
Errors: NOT_VERIFIED, QUOTE_EXPIRED, AMOUNT_BELOW_MIN, AMOUNT_ABOVE_MAX, LIMIT_EXCEEDED, PROVIDER_LIQUIDITY, CONTACTS_REQUIRED, CONTACTS_CONFLICT, CUSTOMER_NOT_FOUND, PROVIDER_UNAVAILABLE, PROVIDER_ERROR; 422 "amountCrypto must be positive".
GET /v4/sbp/orders
The customer's orders, newest first.
| Query | Type | Required | Description |
|---|---|---|---|
limit | number | no | 1–100, default 20 |
Response: { "orders": [ …order objects… ] }.
GET /v4/sbp/orders/{orderId}
One order of the signing customer. An open order is refreshed from the payment provider when it has not been updated recently, so this is the endpoint to poll. Response: an order; another customer's or unknown order — 404 "Order not found".
Order object
{
"orderId": "4f0c7c7e-3a8d-4c55-9b0e-2a51c1f0a6d2",
"provider": "sbp",
"status": "awaiting_payment",
"creditStatus": "none",
"amountUser": "125.500000",
"amountGrossFiat": "10196.88",
"fiatCurrency": "RUB",
"paymentLink": "https://…",
"creditTxHash": null,
"createdAt": "2026-10-12T09:15:02.000Z"
}| Field | Type | Description |
|---|---|---|
orderId | string (UUID) | Order id |
provider | string | Route name, always sbp |
status | string | Payment status — see below |
creditStatus | string | Crediting status — see below |
amountUser | string | USDC to be credited to the customer |
amountGrossFiat | string | null | Fiat amount charged to the customer (RUB) |
fiatCurrency | string | null | RUB |
paymentLink | string | null | SBP payment link; null until it is issued |
creditTxHash | string | null | Rubin transaction that credited the USDC |
createdAt | string | Creation time |
status
| Value | Meaning |
|---|---|
initiated | Created, waiting for the payment provider |
processing | Accepted by the payment provider |
awaiting_payment | Waiting for the customer to pay |
success | Paid |
failed | Rejected or failed |
expired | Not paid in time |
refunded | Payment returned to the customer |
creditStatus
| Value | Meaning |
|---|---|
none | Not paid yet |
pending | Paid, queued for crediting |
submitted | Crediting transaction sent, awaiting confirmation |
credited | USDC is on the customer's main account |
failed | Attempt failed; retried automatically |
manual_review | Needs support — contact us with the orderId |
The order is complete when status is success and creditStatus is credited.
Service
GET /health
{ "ok": true, "service": "onramp" }. Public.