Integration Flow
This page walks through the flow the rubin.trade deposit dialog implements, step by step. Every call is described in detail in the API reference; all customer calls are signed.
customer + flow ─► verification ─► limits + rates ─► order ─► pay by SBP ─► poll until credited1. Check the customer
Start every session with GET /v4/sbp/customer. The first signed request of an address assigns it a payment provider; the address stays with that provider from then on, so verification and limits never get lost.
The response tells where the user is:
| Field | Use |
|---|---|
flow | Which steps this customer needs — see below |
exists | false — the user has not started verification yet |
verificationLevel | 3 or higher — the user can create orders |
identifyStatus | pending — document or eID check in progress; failed — show the form again |
kycStatus | Hosted KYC state: none, pending, approved, rejected, expired |
contactsStatus | Hosted contacts state: none, pending, ready, action_required, failed, review_required |
activeOrderId | The latest open order, if any — resume it instead of creating a new one |
2. Read the flow
Build the screens from the flow descriptor rather than hard-coding them:
| Flag | Meaning for the client |
|---|---|
identity | form — collect phone, SMS code and identity document (or eID) through the API; hosted — send the user to a hosted KYC page |
phoneOtp | The phone number is confirmed with an SMS code |
contacts | After KYC a phone number and an e-mail are also needed (POST /customer/contacts) |
ratesPerCustomer | GET /rates is personal and must be signed |
settlement | Informational, no client action |
poolLimited | Orders are capped by available liquidity, reported as availableUsdc in GET /limits |
Today new customers get the form flow: SMS code, then identity document or government eID (ESIA).
3. Verification
The user enters their own data; the partner only relays it. Rubin does not store identity-document details — they are forwarded to the provider and masked in logs.
Form flow (identity: "form")
| Level | Reached by |
|---|---|
| 0 | No customer yet |
| 2 | Phone confirmed: POST /customer with phone → SMS → POST /customer/verify-phone with the code |
| 3 | Identity confirmed: POST /customer/identify with identity-document details. Orders are allowed from this level. |
| 4 | Government eID (ESIA) login: POST /customer/esia → redirect the user → they return to your redirectUrl |
Notes:
- Send the phone as
+7followed by 10 digits. The SMS code is 4 digits. UsePOST /customer/resend-otpto send it again. verify-phoneanswers200even for a wrong code — judge byverificationLevel: below2means the code was not accepted.identifycan answeridentifyStatus: "pending"— the document is checked asynchronously. PollGET /customerevery few seconds until the level reaches3or the status becomesfailed.- After an ESIA redirect, poll
GET /customeruntil the level reaches4. The return URL must belong to an origin we have allow-listed for you. POST /customer/identify/updatere-sends corrected identity-document details for a customer who is already at level 3.
Hosted flow (identity: "hosted")
POST /customer/kycreturns aurl. Open it in a new tab or a web view; passreturnUrl(HTTPS only) to bring the user back. AnullURL with a non-approvedstatus means the page is still being prepared — retry shortly.- Poll
GET /customeruntilkycStatusisapproved. POST /customer/contactswithphoneandemail. Onaction_requiredopenactionUrl; onpendingpollGET /customeruntilcontactsStatusisready.
The customer is at level 3 once KYC is approved and contacts are ready.
4. Limits
GET /v4/sbp/limits returns what the user may still buy. Block the amount field with, in this order:
amount < minOrderRublesamount > maxOrderRublesamount > remainingRublesamount / userRate > availableUsdc— only whenflow.poolLimitedistrue
Any of these fields may be null, meaning "no such limit reported". Period limits reset on a UTC+3 calendar: daily at midnight, weekly on Monday, monthly on the 1st.
Per-customer ceilings currently depend on the verification level:
| Level | Per day | Per week | Per month |
|---|---|---|---|
| 3 — identity document | 100 000 RUB | 200 000 RUB | 200 000 RUB |
| 4 — government eID | 200 000 RUB | 500 000 RUB | 1 000 000 RUB |
5. Quote and order
GET /v4/sbp/ratesreturnsuserRate— RUB per 1 USDC, final for the customer — plusquoteSignatureandexpiresInSeconds. Refresh the quote before it expires.- Convert the amount:
usdc = rub / userRate, rounded down to 2 decimals. POST /v4/sbp/orderswithamountCrypto(a string, the USDC to credit) and the currentquoteSignature. OnQUOTE_EXPIREDfetch a new quote and retry.
The response is an order. amountGrossFiat is what the user will actually be charged in RUB; show it next to the QR code, since rounding can make it differ slightly from what they typed.
6. Payment
paymentLinkcan benullright after creation — pollGET /orders/{orderId}every 2 seconds until it appears.- On desktop, render
paymentLinkas a QR code; the user scans it with their banking app (SBP). On a phone, open the link directly. - The user can close your window: the order lives on the server, and
GET /ordersreturns it later.
7. Wait for crediting
Poll GET /v4/sbp/orders/{orderId}:
| Order state | Poll every | Show |
|---|---|---|
initiated, processing, awaiting_payment | 2–4 s | QR code / "waiting for payment" |
success with creditStatus pending, submitted or failed | 4 s | "Payment received, crediting…" |
success + creditStatus: "credited" | stop | Done — USDC is on the account |
creditStatus: "manual_review" | stop | Contact support with the orderId |
failed, expired, refunded | stop | Offer to start again |
creditStatus: "failed" is transient — crediting is retried automatically. Only manual_review needs a human.
creditTxHash is the Rubin transaction that credited the USDC; it can change between retries, so treat it as final only once creditStatus is credited.
8. After crediting
The USDC lands on the customer's main account (bank balance), not on a trading subaccount. From there it is a regular Rubin transfer, signed by the customer's key:
- move it to a subaccount to trade (
MsgDepositToSubaccount), or - send it to your platform's account inside Rubin.
Keep a small USDC amount on the main account for network fees (about 0.004 USDC per transfer).
Request limits
SMS and code checks are rate-limited per hour:
| Action | Per address | Per IP | Per phone |
|---|---|---|---|
Send SMS (POST /customer, /resend-otp) | 5 | 20 | 3 |
Check code (POST /customer/verify-phone) | 10 | 30 | — |
Over the limit the API answers 429 with code SMS_RATE_LIMIT and retryAfterSeconds. Wait that long before retrying: the window is fixed, so an earlier retry only gets another 429.
The IP is the address that connected to our edge. X-Forwarded-For, X-Real-IP and similar headers sent by your client or your own proxies are ignored. IPv6 clients are counted per /64.
Testing on testnet
On testnet payments and verification run in sandbox mode, and two extra endpoints are available:
POST /v4/sbp/orders/{orderId}/simulate-paidwith{"outcome": "completed"}marks an order paid (failedandexpiredare also accepted);POST /v4/sbp/customer/kyc/simulatewith{"approved": true}approves hosted KYC (hosted flow only).
These endpoints answer 404 on mainnet.