Приём без нашей страницы (H2H)
Создайте платёж запросом со своего сервера и получите в ответ код СБП. Покупатель остаётся на вашем сайте: QR и кнопку «Оплатить в приложении» вы рисуете сами, наша страница оплаты не участвует.
API v1 · обновлено 2026-09-12
Когда это нужно
Обычная интеграция отдаёт покупателя на нашу страницу оплаты — это проще и не требует ничего, кроме редиректа. H2H нужен, когда оплата должна целиком происходить у вас: своё оформление, свой экран ожидания, свой QR внутри приложения или бота. Взамен вы берёте на себя показ кода, таймер и опрос статуса.
Сейчас так работает СБП. Приём картами в этом режиме готовится; до его запуска карты принимаются через страницу оплаты.
Обязательное условие: IP покупателя
В поле payerIp передавайте публичный адрес покупателя, а не адрес своего сервера. Банк в СБП оценивает по нему риск платежа и без него платёж отклоняет. При серверной интеграции мы видим только ваш сервер, поэтому адрес можем получить лишь от вас.
Берите его из заголовка вашего балансировщика (X-Forwarded-For, первый адрес) или из соединения с браузером. Адреса из приватных диапазонов (10.*, 192.168.*, 172.16–31.*, 127.*) мы отклоняем сразу. IPv6 принимается.
Одним запросом: счёт и код сразу
Добавьте к обычному созданию счёта поля method, payerIp и start: true — в ответе появится объект payment с кодом СБП.
Объект payment
| Поле | Описание |
|---|---|
qr | Строка для QR-кода. Нарисуйте её любой библиотекой QR и покажите покупателю. Это ссылка СБП, картинку мы не отдаём — так вы сами выбираете размер и оформление. |
deeplink | Та же ссылка для кнопки «Оплатить в приложении банка» — на телефоне открывает приложение. На СБП совпадает с qr. |
url | Страница, на которую нужно отправить покупателя, если у метода нет QR. Для СБП — null. |
startedAt | Когда код выдан. |
expiresAt | Срок жизни кода — он короче срока счёта. Именно его показывайте в таймере, см. ниже. |
payment: null означает, что платёж ещё не создан (счёт в статусе created) или уже закрыт. Поле startError непустое, если счёт создался, а платёж — нет: сам счёт при этом живой, его id сохранён, повторите /start.
Двумя запросами: счёт заранее, код по клику
Так удобнее, когда заказ оформляется раньше оплаты: счёт создаётся при оформлении, а код запрашивается в момент, когда покупатель выбрал способ. Срок жизни кода (минуты) тогда не тратится на размышления покупателя.
| Поле | Тип | Описание |
|---|---|---|
method обяз. | sbp | Какой платёж создавать. Если при создании счёта способ был зафиксирован, он должен совпадать. |
payerIp обяз. | string | Публичный адрес покупателя (выше). |
payerEmail | string | Почта покупателя, если нужна для чека. |
Ответ — тот же счёт с объектом payment, что и при start: true.
Повторный вызов не выдаёт новый код
Пока платёж жив, /start с тем же способом возвращает тот же код. Это сделано намеренно: отменить уже выданный код у банка нечем, и каждый новый QR — это ещё один код, по которому покупатель может заплатить. Вызывайте /start по действию покупателя, а не в цикле.
Таймер и перевыпуск кода
Код СБП живёт минуты, а счёт — до суток. Показывайте покупателю payment.expiresAt, а не expiresAt счёта: иначе человек увидит часовой таймер, уйдёт пить чай и вернётся к мёртвому коду.
Когда код перестал действовать, счёт возвращается в статус created и остаётся годным: запросите /start ещё раз и покажите новый код. Так можно сделать до трёх раз, дальше счёт закрывается статусом failed — создайте новый. Проверять, что код умер, нужно по статусу счёта: пока он pending, действует выданный код.
Итог платежа
Источник истины — вебхук invoice.paid. Опрос нужен для экрана ожидания: пока покупатель смотрит на QR, спрашивайте статус счёта не чаще одного раза в 3 секунды.
Товар отдавайте по статусу paid. Статусы failed и expired — финальные, схема переходов та же, что у обычной интеграции.
Ошибки
| Код | Когда | Что делать |
|---|---|---|
payer_ip_required | start: true без payerIp. | Передайте адрес покупателя. |
payer_ip_invalid | Приватный адрес или адрес вашего сервера. | Возьмите реальный адрес из заголовка балансировщика. |
validation | start: true без method. | Укажите способ оплаты. |
method_unavailable | Способ не подключён проекту или временно недоступен. | Предложите покупателю другой способ; доступные — в GET /methods. |
invoice_expired | Срок счёта истёк. | Создайте новый счёт. |
invoice_closed | Счёт уже оплачен, отклонён или истёк. | Проверьте статус; для оплаченного ничего делать не нужно. |
provider_unavailable | Платёж не удалось создать прямо сейчас. | Повторите /start через несколько секунд или предложите другой способ. |
not_found | Счёт не ваш или ключ другого режима (тестовый против боевого). | Проверьте id и ключ. |
Перед запуском
- В
payerIpуходит настоящий адрес покупателя — проверьте на реальном трафике, а не на localhost. - Таймер у покупателя считает по
payment.expiresAt. /startвызывается по действию покупателя, не в цикле и не на каждый опрос статуса.- Статус опрашивается не чаще раза в 3 секунды, а товар отдаётся по вебхуку.
- Сценарий «код умер, покупатель хочет ещё раз» реализован: счёт вернулся в
created→ новый/start. - Всё проверено тестовым ключом: там код выдаёт эмулятор, деньги не двигаются.