SBP payments¶
The payer sends money by phone number through the Faster Payments System (SBP) in their bank app. You create a payment, the API issues the recipient's phone number and name, you show them to the payer and wait for the result.
- Your server → API
POST /api/v1/paymentswithpayment_method=sbp - APIPicks requisites, usually within 15 seconds
- API ⇢ Your server
201:status=processing,requisite=+7…,holder_name - Your server → PayerPhone number, recipient, amount and deadline
- PayerTransfers via SBP in their bank app
- API → Your serverCallback:
completedorcanceled - Your server ⇢ APIYou reply
2xx
Create a payment¶
POST /api/v1/payments — a signed request with a JSON body.
POST {{base_url}}/api/v1/payments HTTP/1.1
Authorization: Bearer nl_test_...
Content-Type: application/json
Content-Digest: sha-256=:…:
X-Request-ID: 0b6d2c4e-8f1a-4d3b-9e7c-5a2f1b3c4d5e
X-Request-Nonce: 9f86d081884c7d659a2feaa0c55ad015
Signature-Input: sig1=(…);created=…;expires=…;keyid="ed25519-…";alg="ed25519"
Signature: sig1=:…:
{"merchant_payment_id":"order-42","amount":3400,"payment_method":"sbp","bank_code":"sber","callback_url":"https://merchant.example/callbacks"}
Request fields¶
| Field | Type | Req. | Rules |
|---|---|---|---|
merchant_payment_id |
string | yes | Your order id. 1–64 characters A-Za-z0-9._:-. Unique per project: it is the idempotency key |
amount |
number or string | yes | Amount in major currency units: 3400 or "3400.50". Positive, at most 6 decimals. A string is safer than a number: no rounding errors |
payment_method |
string | yes | sbp |
currency |
string | no | Currency code. Default RUB |
geo_code |
string | no | Country, two letters. Default RU |
bank_code |
string | no | The payer's bank — a lower-case code from our bank catalog: sber, tinkoff, ozon. Not a BIC. Empty means any bank |
is_intrabank |
boolean | no | true for a transfer within one bank. Default false |
callback_url |
string | no | Where to send this payment's callback. Absolute https://, up to 2048 characters, no credentials, public host. Otherwise the project URL is used |
No other fields are accepted: an unknown field returns 400 bad_request. Test mode is a
project setting, not a request field. A request without currency and geo_code and the
same request with "RUB" and "RU" are the same request.
There is no deadline in the request: how long a payment lives is a project setting, 15 minutes from creation by default. Your manager can change it.
Response¶
201 Created for a new payment. 200 OK for a repeat with the same
merchant_payment_id and the same body: the existing payment is returned, no new one is
created.
{
"id": "3f1c2b9e-6a8d-4c4b-9a71-2c7f5d1e0a42",
"merchant_payment_id": "order-42",
"flow": "runtime_live",
"status": "processing",
"amount": 3400,
"initial_amount": 3400,
"currency": "RUB",
"geo_code": "RU",
"payment_method": "sbp",
"bank_code": "sber",
"is_intrabank": false,
"callback_url": "https://merchant.example/callbacks",
"is_test": false,
"merchant_information": {
"course": 100.00,
"rate": 11.00,
"amount_usdt": "34.0000",
"amount_rate": "3026.0000",
"amount_usdt_rate": "30.2600"
},
"requisite": "+79991234567",
"holder_name": "Иван Петров",
"created_at": "2026-09-29T09:30:00Z",
"updated_at": "2026-09-29T09:30:01Z"
}
| Field | Meaning |
|---|---|
id |
Payment id. Store it next to the order |
merchant_payment_id |
Your order id |
flow |
runtime_live — a regular payment, project_test — a project test payment |
status |
Current status, see below |
amount |
Amount to pay, in major currency units. May change after a payment re-check |
initial_amount |
Amount from the request |
currency, geo_code, payment_method, is_intrabank |
As in the request, with defaults applied |
bank_code |
The payer's bank from the request, or null. Not the requisite's bank |
callback_url |
Callback URL from the request, or null |
is_test |
true for a project test payment |
merchant_information |
Settlement in USDT; null until requisites are issued |
requisite |
Recipient phone number for the SBP transfer; null until issued |
holder_name |
Recipient name as the payer's bank will show it, or null |
qrcode_link, deeplink_url |
Only when the requisites have them: you can show a QR code or an "Open bank app" button |
error_code, error_comment |
Only for a payment in error: why no requisites were issued |
created_at, updated_at |
Creation and last change time, UTC |
bank_code, callback_url, merchant_information, requisite and holder_name are always
present; the value may be null. New fields may be added without a new API version:
ignore unknown fields.
Settlement merchant_information¶
What you receive for the payment. The exchange rate and fee are fixed when requisites are issued and do not change afterwards: settlement is not recalculated at current rates. After a payment re-check, settlement uses the new amount with the same rate and fee. These are the same figures that are credited to the project balance.
| Field | Meaning | Type, decimals |
|---|---|---|
course |
Rate: units of the payment currency per 1 USDT | number, 2 |
rate |
Project fee, % | number, 2 |
amount_usdt |
Payment amount in USDT | string, 4 |
amount_rate |
Amount after the fee, in the payment currency | string, 4 |
amount_usdt_rate |
Amount after the fee in USDT — credited to the project balance | string, 4 |
course and rate are numbers with exactly two decimals (100.00); amounts are decimal
strings. Extra digits are truncated, never rounded up; the fee is rounded down. In the example above: 3400 RUB at a
rate of 100 and an 11% fee is 34 USDT, a fee of 374 RUB (3.74 USDT), 30.26 USDT credited.
Parse course and rate as decimals
Use a decimal type (Decimal, BigDecimal, json.Number), not a float, to keep
the precision.
Do not recompute amount_usdt
The rate may be stored with more than two decimals, so amount / course can
differ from amount_usdt in the last digit. amount_usdt is authoritative.
What to show the payer¶
Show the requisites only when the status is processing and requisite is present. The
payer transfers exactly amount to requisite.
- Phone number from
requisite— large, with a "Copy" button. - Recipient from
holder_name: the payer checks the name in the bank app before sending. - Amount — exactly
amount, to the kopeck. Warn that a different amount may not be credited automatically. - Deadline — a countdown from
created_atover your project's payment lifetime (15 minutes by default). After it, the requisites must not be used. - Next step — "Come back to this page after the transfer". Update the status from the callback or by polling.
Do not cache requisites
Requisites are issued for one payment. For a new order or another attempt, create a
new payment with a new merchant_payment_id.
Statuses¶
| Status | Meaning | What to do |
|---|---|---|
| created | Accepted, no requisites yet | Wait. The response usually already says processing |
| processing | Requisites issued, waiting for the transfer | Show the requisites to the payer |
| completed | Payment confirmed | Deliver the goods or service |
| canceled | Not paid: the lifetime ran out, it was canceled, or the payment could not be confirmed | Offer to pay again with a new payment |
| error | No requisites were issued, reason in error_code |
Offer to pay again with a new payment |
| appeal | A closed payment is being re-checked | Wait for the resolution: a callback follows if it ends in completed or canceled |
error happens only before requisites are issued. From processing a payment goes only
to completed or canceled: if the payment cannot be confirmed after requisites were
issued, it is also canceled, with no error_code. completed, canceled and error are
final. Only a payment re-check or a support decision can change them.
All error codes are in
Statuses and errors.
Getting the result¶
Callbacks are the primary channel. When a payment becomes completed or canceled,
the API sends a POST to your URL. Verification and handling are in
Callbacks. There is no callback for error: it happens only before
requisites are issued and is visible in the create response and when you read the payment.
Once the payer has seen requisites, the result arrives as a callback.
Polling is the safety net. If no callback arrived, read the payment:
curl -sS "{{base_url}}/api/v1/payments/3f1c2b9e-6a8d-4c4b-9a71-2c7f5d1e0a42" \
-H "Authorization: Bearer $API_TOKEN"
The response has the same shape as on create. If the project policy has
signature_required, sign the GET too (empty body).
Recommended:
- The payer's checkout page polls your server, not the API.
- Your server reads the payment from the API only when a callback is missing, for
example once a minute for
processingpayments past the payment lifetime. - Do not poll a payment more often than every 5–10 seconds: the project shares one rate limit.
Edge cases¶
Response with status error right away. 201 with status: "error" means the
payment was saved but no requisites were issued. The reason is in error_code
(provider_no_requisites, provider_result_ambiguous). No callback follows for this
payment. There is nothing to show the payer — offer to try later with a new payment.
422 on create. The payment cannot be processed: payment_not_covered (method or
amount is outside your contract), payment_rate_unavailable (no exchange rate),
payment_not_routable (the payment cannot be accepted with this method right now). If the payment was saved, the
body carries payment_id: the payment stays in error, the id is taken, and repeating the
same request returns it (200). A new attempt needs a new id. Without payment_id no
payment was created and the id is free.
Timeout or dropped connection. You do not know whether the payment exists. Repeat
the same request with the same merchant_payment_id and a fresh nonce and signature:
if the payment exists you get it back (200), otherwise it is created. No duplicate is
possible.
The payer sent a different amount. The payment may be re-checked and the actual
amount confirmed. You then receive a completed callback with the new amount: amount
changes, initial_amount stays. Fulfil the order by the callback amount.
Deadline passed. A payment not paid within its lifetime becomes canceled, and a
callback is sent. If the payer paid after the deadline, the payment is reviewed manually:
a confirmed payment brings a new completed callback.
The payment could not be confirmed after requisites were issued. The payment then
becomes canceled with no error_code, and a callback is sent. If the money did arrive, the payment is reviewed
manually and a new completed callback follows.
Slow response. Requisites are picked during your request. Keep the client timeout at 15 seconds or more.
See also¶
- Card payments — the same, with a card transfer.
- Idempotency and limits — retries and rate limits.
- API reference — field schema.