# Приём по СБП

Плательщик переводит деньги по номеру телефона через Систему быстрых платежей из
приложения своего банка. Вы создаёте заявку, API выдаёт номер телефона
получателя и имя, вы показываете их плательщику и ждёте результат.

<div class="pp-seq" data-lanes="Ваш сервер|API|Плательщик" markdown>

1. **Ваш сервер → API.** `POST /api/v1/payments` с `payment_method=sbp`
2. **API.** Подбирает реквизиты, обычно до 15 секунд
3. **API ⇢ Ваш сервер.** `201`: `status=processing`, `requisite=+7…`, `holder_name`
4. **Ваш сервер → Плательщик.** Номер телефона, получатель, сумма и срок
5. **Плательщик.** Переводит по СБП в приложении своего банка
6. **API → Ваш сервер.** Callback: `completed` или `canceled`
7. **Ваш сервер ⇢ API.** Отвечаете `2xx`

</div>

## Создать заявку { #create }

`POST /api/v1/payments` — [подписанный](signing.md) запрос с JSON-телом.

```http
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 }

| Поле | Тип | Обяз. | Правила |
| --- | --- | --- | --- |
| `merchant_payment_id` | строка | да | Ваш номер заказа. 1–64 символа `A-Za-z0-9._:-`. Уникален в проекте: это [ключ идемпотентности](idempotency.md) |
| `amount` | число или строка | да | Сумма в основных единицах валюты: `3400` или `"3400.50"`. Больше нуля, не больше 6 знаков после точки. Строка надёжнее числа: без ошибок округления |
| `payment_method` | строка | да | `sbp` |
| `currency` | строка | нет | Код валюты. По умолчанию `RUB` |
| `geo_code` | строка | нет | Страна, две буквы. По умолчанию `RU` |
| `bank_code` | строка | нет | Банк плательщика — код из справочника в нижнем регистре: `sber`, `tinkoff`, `ozon`. Это не БИК. Пусто — любой банк |
| `is_intrabank` | логическое | нет | `true` — перевод внутри одного банка. По умолчанию `false` |
| `callback_url` | строка | нет | Куда слать callback по этой заявке. Абсолютный `https://`, до 2048 символов, без логина и пароля, публичный хост. Иначе — адрес проекта |

Другие поля не принимаются: неизвестное поле — `400 bad_request`. Тестовый режим
задаётся настройкой проекта, а не полем запроса. Запрос без `currency` и `geo_code` и тот же
запрос с `"RUB"` и `"RU"` — один и тот же запрос.

Срока оплаты в запросе нет: сколько живёт заявка, задаёт настройка проекта — по умолчанию
15 минут с момента создания. Изменить срок можно через вашего менеджера.

### Ответ { #response }

`201 Created` — новая заявка. `200 OK` — повтор с тем же `merchant_payment_id`
и тем же телом: вернулась уже созданная заявка, новой не появилось.

```json
{
  "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"
}
```

| Поле | Что значит |
| --- | --- |
| `id` | Идентификатор заявки. Сохраните его рядом с заказом |
| `merchant_payment_id` | Ваш номер заказа |
| `flow` | `runtime_live` — обычная заявка, `project_test` — тестовая заявка проекта |
| `status` | Текущий статус, см. [ниже](#statuses) |
| `amount` | Сумма к оплате, в основных единицах валюты. Может измениться после [проверки оплаты](#edge-cases) |
| `initial_amount` | Сумма из запроса |
| `currency`, `geo_code`, `payment_method`, `is_intrabank` | Как в запросе, с учётом значений по умолчанию |
| `bank_code` | Банк плательщика из запроса или `null`. Это не банк реквизита |
| `callback_url` | Адрес callback-а из запроса или `null` |
| `is_test` | `true` у тестовой заявки проекта |
| `merchant_information` | [Расчёт по заявке](#merchant-information) в USDT; `null`, пока реквизиты не выданы |
| `requisite` | Номер телефона получателя для перевода по СБП; `null`, пока не выдан |
| `holder_name` | Имя получателя, как его покажет банк плательщика, или `null` |
| `qrcode_link`, `deeplink_url` | Только если они есть у реквизитов: можно показать QR-код или кнопку «Открыть банк» |
| `error_code`, `error_comment` | Только у заявки в статусе `error`: почему реквизиты не выданы |
| `created_at`, `updated_at` | Время создания и последнего изменения, UTC |

Поля `bank_code`, `callback_url`, `merchant_information`, `requisite` и `holder_name` есть в
ответе всегда, значение может быть `null`. Новые поля могут добавляться без смены версии
API: неизвестные поля игнорируйте.

### Расчёт `merchant_information` { #merchant-information }

Сколько вы получите по заявке. Курс и ставка фиксируются, когда выдаются реквизиты, и потом
не меняются: по текущему курсу расчёт не пересчитывается. После проверки оплаты расчёт идёт по
новой сумме с теми же курсом и ставкой. Это те же цифры, что зачисляются на баланс проекта.

| Поле | Что значит | Тип, знаков после точки |
| --- | --- | --- |
| `course` | Курс: сколько единиц валюты заявки за 1 USDT | число, 2 |
| `rate` | Ставка комиссии проекта, % | число, 2 |
| `amount_usdt` | Сумма заявки в USDT | строка, 4 |
| `amount_rate` | Сумма за вычетом комиссии, в валюте заявки | строка, 4 |
| `amount_usdt_rate` | Сумма за вычетом комиссии в USDT — её зачислят на баланс проекта | строка, 4 |

`course` и `rate` — числа ровно с двумя знаками (`100.00`), суммы — десятичные строки.
Лишние знаки отбрасываются, а не округляются вверх; комиссия считается с округлением вниз.
В примере выше: 3400 RUB по курсу 100 и ставке 11% — 34 USDT, комиссия 374 RUB (3,74 USDT),
к зачислению 30,26 USDT.

!!! tip "Читайте `course` и `rate` как decimal"
    Разбирайте их десятичным типом (`Decimal`, `BigDecimal`, `json.Number`), а не float —
    иначе можно потерять точность.

!!! warning "Не пересчитывайте `amount_usdt` сами"
    Курс может храниться точнее двух знаков, поэтому `amount / course` иногда
    расходится с `amount_usdt` в последнем знаке. Верное значение — `amount_usdt`.

## Что показать плательщику { #payer-screen }

Показывайте реквизиты, только когда статус `processing` и есть `requisite`. Плательщик
переводит ровно `amount` на `requisite`.

- **Номер телефона** из `requisite` — крупно, с кнопкой «Скопировать».
- **Получатель** из `holder_name`: плательщик сверит имя в приложении банка перед переводом.
- **Сумма** — ровно `amount`, до копейки. Предупредите: перевод другой суммы может
  не зачесться автоматически.
- **Срок** — обратный отсчёт от `created_at` на срок заявки вашего проекта (по умолчанию
  15 минут). После срока реквизиты использовать нельзя.
- **Что дальше** — «После перевода вернитесь на эту страницу». Статус обновляйте по
  callback-у или опросом.

!!! warning "Не кешируйте реквизиты"
    Реквизиты выдаются на одну заявку. Для нового заказа или повторной попытки
    создайте новую заявку с новым `merchant_payment_id`.

## Статусы { #statuses }

<div class="pp-statusmap" data-lang="ru"></div>

| Статус | Что значит | Что делать |
| --- | --- | --- |
| <span class="pp-status">created</span> | Заявка принята, реквизиты ещё не выданы | Ждать. Обычно ответ сразу приходит уже в `processing` |
| <span class="pp-status pp-status--wait">processing</span> | Реквизиты выданы, ждём перевод | Показать реквизиты плательщику |
| <span class="pp-status pp-status--ok">completed</span> | Оплата подтверждена | Выдать товар или услугу |
| <span class="pp-status">canceled</span> | Не оплачено: срок заявки истёк, заявку отменили или оплату не удалось подтвердить | Предложить оплатить заново — новой заявкой |
| <span class="pp-status pp-status--bad">error</span> | Реквизиты не выданы, причина в `error_code` | Предложить оплатить заново — новой заявкой |
| <span class="pp-status pp-status--warn">appeal</span> | Оплату по закрытой заявке перепроверяют | Ждать решения: если итог — `completed` или `canceled`, придёт callback |

`error` бывает только до выдачи реквизитов. Из `processing` заявка переходит только в
`completed` или `canceled`: если после выдачи реквизитов оплату не удалось подтвердить —
тоже `canceled`, без `error_code`. `completed`, `canceled` и `error` — финальные. Изменить их
может только проверка оплаты или решение службы поддержки. Все коды ошибок — в разделе
[Статусы и ошибки](statuses.md).

## Как узнать результат { #result }

**Callback — основной способ.** Когда заявка становится `completed` или `canceled`,
API отправляет `POST` на ваш адрес. Как проверить подпись и обработать — в разделе
[Callback-и](callbacks.md). О статусе `error` callback не приходит: он бывает только до
выдачи реквизитов и виден в ответе на создание и при чтении заявки. Если плательщик уже
увидел реквизиты, результат придёт callback-ом.

**Опрос — страховка.** Если callback не пришёл, прочитайте заявку:

```bash
curl -sS "{{base_url}}/api/v1/payments/3f1c2b9e-6a8d-4c4b-9a71-2c7f5d1e0a42" \
  -H "Authorization: Bearer $API_TOKEN"
```

Ответ — та же форма, что при создании. Если в политике проекта включено
`signature_required`, подпишите и `GET` (тело пустое).

Рекомендуем так:

- Страница оплаты у плательщика опрашивает **ваш** сервер, а не API.
- Ваш сервер запрашивает заявку у API, только если callback не пришёл: например,
  раз в минуту для заявок в `processing`, у которых срок заявки уже прошёл.
- Не опрашивайте чаще раза в 5–10 секунд на заявку: у проекта общий
  [лимит запросов](idempotency.md#rate-limits).

## Крайние случаи { #edge-cases }

**Ответ сразу со статусом `error`.** `201` со `status: "error"` значит, что заявка
сохранена, но реквизитов нет. Причина — в `error_code` (`provider_no_requisites`,
`provider_result_ambiguous`). Callback-а об этой заявке не будет. Плательщику нечего
показывать — предложите попробовать позже, новой заявкой.

**`422` при создании.** Заявку нельзя провести: `payment_not_covered` (метод или сумма
вне вашего договора), `payment_rate_unavailable` (нет курса), `payment_not_routable`
(сейчас платёж этим способом принять нельзя). Если заявка успела сохраниться, в теле есть
`payment_id`: заявка осталась в статусе `error`, номер занят, и повтор того же запроса
вернёт её (`200`). Для новой попытки нужен новый номер. Без `payment_id` заявка не
создана, и номер свободен.

**Таймаут или обрыв связи.** Вы не знаете, создана ли заявка. Повторите **тот же** запрос
с тем же `merchant_payment_id` и новыми nonce и подписью: если заявка есть, вернётся
она (`200`), если нет — создастся. Двойной заявки не будет.

**Плательщик перевёл другую сумму.** Оплату могут перепроверить и подтвердить
фактическую сумму. Тогда придёт callback с `completed` и новой суммой: `amount`
изменится, `initial_amount` останется прежним. Выдавайте заказ по сумме из callback-а.

**Срок истёк.** Неоплаченная к концу срока заявка становится `canceled`, и приходит
callback. Если плательщик перевёл деньги после срока, заявку разберут вручную: при
подтверждённой оплате придёт новый callback `completed`.

**Оплату не удалось подтвердить после выдачи реквизитов.** Тогда заявка становится
`canceled` без `error_code`, и приходит callback. Если оплата всё же прошла, её разберут вручную: придёт новый callback `completed`.

**Долгий ответ.** Реквизиты подбираются прямо во время запроса. Держите
таймаут клиента не меньше 15 секунд.

## Смотрите также

- [Приём по карте](card.md) — то же, но перевод на карту.
- [Идемпотентность и лимиты](idempotency.md) — повторы запросов и лимиты.
- [API reference](api-reference.md) — схема полей.