Skip to content

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 credited

1. 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:

FieldUse
flowWhich steps this customer needs — see below
existsfalse — the user has not started verification yet
verificationLevel3 or higher — the user can create orders
identifyStatuspending — document or eID check in progress; failed — show the form again
kycStatusHosted KYC state: none, pending, approved, rejected, expired
contactsStatusHosted contacts state: none, pending, ready, action_required, failed, review_required
activeOrderIdThe 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:

FlagMeaning for the client
identityform — collect phone, SMS code and identity document (or eID) through the API; hosted — send the user to a hosted KYC page
phoneOtpThe phone number is confirmed with an SMS code
contactsAfter KYC a phone number and an e-mail are also needed (POST /customer/contacts)
ratesPerCustomerGET /rates is personal and must be signed
settlementInformational, no client action
poolLimitedOrders 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")

LevelReached by
0No customer yet
2Phone confirmed: POST /customer with phone → SMS → POST /customer/verify-phone with the code
3Identity confirmed: POST /customer/identify with identity-document details. Orders are allowed from this level.
4Government eID (ESIA) login: POST /customer/esia → redirect the user → they return to your redirectUrl

Notes:

  • Send the phone as +7 followed by 10 digits. The SMS code is 4 digits. Use POST /customer/resend-otp to send it again.
  • verify-phone answers 200 even for a wrong code — judge by verificationLevel: below 2 means the code was not accepted.
  • identify can answer identifyStatus: "pending" — the document is checked asynchronously. Poll GET /customer every few seconds until the level reaches 3 or the status becomes failed.
  • After an ESIA redirect, poll GET /customer until the level reaches 4. The return URL must belong to an origin we have allow-listed for you.
  • POST /customer/identify/update re-sends corrected identity-document details for a customer who is already at level 3.

Hosted flow (identity: "hosted")

  1. POST /customer/kyc returns a url. Open it in a new tab or a web view; pass returnUrl (HTTPS only) to bring the user back. A null URL with a non-approved status means the page is still being prepared — retry shortly.
  2. Poll GET /customer until kycStatus is approved.
  3. POST /customer/contacts with phone and email. On action_required open actionUrl; on pending poll GET /customer until contactsStatus is ready.

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:

  1. amount < minOrderRubles
  2. amount > maxOrderRubles
  3. amount > remainingRubles
  4. amount / userRate > availableUsdc — only when flow.poolLimited is true

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:

LevelPer dayPer weekPer month
3 — identity document100 000 RUB200 000 RUB200 000 RUB
4 — government eID200 000 RUB500 000 RUB1 000 000 RUB

5. Quote and order

  1. GET /v4/sbp/rates returns userRate — RUB per 1 USDC, final for the customer — plus quoteSignature and expiresInSeconds. Refresh the quote before it expires.
  2. Convert the amount: usdc = rub / userRate, rounded down to 2 decimals.
  3. POST /v4/sbp/orders with amountCrypto (a string, the USDC to credit) and the current quoteSignature. On QUOTE_EXPIRED fetch 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

  • paymentLink can be null right after creation — poll GET /orders/{orderId} every 2 seconds until it appears.
  • On desktop, render paymentLink as 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 /orders returns it later.

7. Wait for crediting

Poll GET /v4/sbp/orders/{orderId}:

Order statePoll everyShow
initiated, processing, awaiting_payment2–4 sQR code / "waiting for payment"
success with creditStatus pending, submitted or failed4 s"Payment received, crediting…"
success + creditStatus: "credited"stopDone — USDC is on the account
creditStatus: "manual_review"stopContact support with the orderId
failed, expired, refundedstopOffer 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:

ActionPer addressPer IPPer phone
Send SMS (POST /customer, /resend-otp)5203
Check code (POST /customer/verify-phone)1030—

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-paid with {"outcome": "completed"} marks an order paid (failed and expired are also accepted);
  • POST /v4/sbp/customer/kyc/simulate with {"approved": true} approves hosted KYC (hosted flow only).

These endpoints answer 404 on mainnet.