Оплата¶
Покупка кредитных пакетов за деньги. Платежи проводит процессинговый центр Paygine (карта и СБП, hosted-страницы ПЦ): приложение никогда не касается карточных данных — оно открывает страницу оплаты ПЦ и ждёт итог.
Здесь — разовые пакеты (включая «Демо»). Регулярное пополнение с автосписанием описано отдельно: Подписки; первый период там оплачивается ровно так же, как разовая покупка на этой странице. Из способов оплаты доступны карта и СБП; SberPay/ЮMoney и прочее — предмет отдельного согласования с ПЦ.
Флоу целиком¶
GET /v1/payments/products → витрина пакетов
POST /v1/payments → 201 {id, payment_url, …}
открыть payment_url → страница оплаты ПЦ (карта с 3DS или QR СБП)
ПЦ вернёт плательщика на страницу /return (успех) или /fail (отказ) —
их URL надо перехватывать, см. «Возврат плательщика»
POST /v1/payments/{id}/refresh → «плательщик вернулся, спросите ПЦ сейчас»
GET /v1/payments/{id}/stream → SSE до исхода (поллинг {id} — запасной путь)
Сервер узнаёт итог от ПЦ по подписанному вебхуку и дополнительно сверяет заказ
сервер-сервер; кредиты зачисляются на сервере — приложению
достаточно дождаться succeeded и перечитать баланс. Даже если вебхук потерялся,
фоновая сверка добьёт статус сама (о сроках — в разделе «Ожидание итога»).
Витрина¶
{
"items": [
{ "id": "…", "code": "demo", "title": "Демо · 100 кредитов", "price_minor": 9900,
"credits": 100, "badge": "Единоразово" }
],
"total": 1, "limit": 1, "offset": 0
}
- Требует авторизации. Без токена —
401. Показать пейволл до логина нельзя. price_minor— цена в копейках (9900 = 99 ₽).- Порядок элементов — порядок витрины, задаётся в админке.
badge— необязательная метка («Популярный»), может бытьnull; рендер на усмотрение клиента.- Витрина не постраничная:
total/limit/offsetописывают единственную страницу и нужны только ради общей для всех списков API формы ответа. Читайтеitems. - Скидочных позиций здесь нет. Товар, который продаётся только по спецпредложению, в витрину не попадает и приходит внутри самого предложения — иначе скидку видели бы все и она перестала бы быть скидкой.
- Пустой список — не признак того, что платежи выключены. Пакеты заводит оператор
в админке; на свежем стенде их может не быть вовсе. Проверить доступность платежей
можно только попыткой создать платёж (см.
503ниже).
Создание платежа¶
curl -X POST "$BASE_URL/v1/payments" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"product_id": "<uuid>", "method": "card"}' # card | sbp
Успех — 201 Created:
{
"id": "…", "status": "created", "product_id": "…",
"amount_minor": 9900, "currency": 643, "credits": 100, "method": "card",
"created_at": "…", "paid_at": null, "receipt_url": null,
"payment_url": "https://test.paygine.com/webapi/Purchase?sector=…&id=…&signature=…"
}
currency— числовой код ISO 4217;643— рубль.paid_atзаполняется в момент зачисления кредитов, до этогоnull.receipt_url— ссылка на электронный кассовый чек (54-ФЗ), появляется вместе со статусомsucceeded;nullдо оплаты и в окружениях без фискализации. Показывайте как «Чек» в деталях платежа, открывается в браузере. Первые минуты после оплаты ссылка может открывать страницу «Данных пока нет»: чеки у ОФД формируются в очереди (до ~10 минут), после чего та же ссылка ведёт уже на сам чек. Ссылка постоянная, перезапрашивать платёж ради неё не нужно.payment_urlоткрывается плательщику (Custom Tab / SFSafariViewController / WebView). Дляcardэто форма карты с 3DS, дляsbp— страница с QR-кодом СБП (на телефоне QR кликабелен и ведёт в приложение банка).- Язык страницы оплаты сервер выбирает по
localeпрофиля:en…→ английский, иначе русский.
payment_url приходит только в ответе на POST
GET /v1/payments/{id} её не возвращает. Сохраните её на время сессии оплаты.
Получить ссылку повторно можно одним способом: повторить POST с тем же
Idempotency-Key. Если запрос ушёл без ключа и ссылка потеряна,
остаётся создать новый платёж; старый доживёт до expired и всё это время будет
занимать один из пяти слотов (см. 429).
Запрос идёт в ПЦ синхронно и может занять до ~10 секунд (сервер ходит к ПЦ через прокси). Ставьте таймаут не меньше 15 секунд: при более коротком вы получите ошибку на клиенте, хотя платёж уже создан и слот занят.
Повтор без второго платежа¶
Создание платежа - самый долгий запрос в API, и ответ на него теряется чаще других.
Шлите его с заголовком Idempotency-Key, ключ создаётся на одно нажатие «Оплатить»:
curl -X POST "$BASE_URL/v1/payments" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: 9f3c2a71-5b0e-4d68-a1c4-7e2d8b6f0a35" \
-d '{"product_id": "<uuid>", "method": "card"}'
Повтор с тем же ключом и тем же телом возвращает тот же платёж с той же
payment_url: второй заказ в ПЦ не заводится, второй слот не занимается, лимит покупок
пакета не расходуется. В ответе текущий status платежа: если плательщик успел оплатить,
придёт succeeded, и открывать страницу уже не нужно.
| Ответ на повтор | Что это значит | Что делать |
|---|---|---|
201 с тем же id |
первый запрос дошёл до ПЦ | открыть payment_url |
409 request_in_progress |
первый запрос ещё ждёт ответа ПЦ | повторить через 1-2 секунды с тем же ключом |
409 idempotency_key_reused |
ключ уже приходил с другим product_id или method |
ошибка в приложении: новое нажатие, новый ключ |
503 provider_unavailable |
ПЦ не зарегистрировал заказ, платёж закрыт | повторить позже, ключ тот же: он свободен |
Общие правила ключа (формат, срок, область) - Ошибки, «Повтор запроса».
Заголовок необязателен: без него каждый POST создаёт новый платёж.
Заказ живёт 30 минут — после этого ПЦ его не оплатит.
Ошибки создания¶
| Код | error.code |
Когда |
|---|---|---|
401 |
auth_required |
нет/протух токен |
403 |
forbidden |
исчерпан лимит покупок пакета на аккаунт; либо пакет продаётся только по спецпредложению, а оно истекло, уже оплачено или не выдавалось |
403 |
registration_required |
запрос от гостя: оплата требует полноценного аккаунта |
404 |
not_found |
нет такого пакета, либо он выключен |
409 |
idempotency_key_reused |
Idempotency-Key уже приходил с другим телом запроса |
409 |
request_in_progress |
первый запрос с этим Idempotency-Key ещё ждёт ответа ПЦ |
422 |
validation_error |
method не card/sbp, product_id не UUID, Idempotency-Key не по формату |
429 |
rate_limited |
слишком часто, либо слишком много незакрытых платежей |
503 |
provider_unavailable |
платежи выключены или ПЦ недоступен |
Про 429 важны две разные причины:
- Частота запросов — общий бюджет записи. Ответ несёт
Retry-After. - Незакрытые платежи — их одновременно может быть не больше пяти.
Заголовка
Retry-Afterв этом случае нет. Слот освобождается сам, когда платёж становится терминальным, максимум через 30 минут. Ретраить раньше бессмысленно — предложите пользователю завершить или подождать.
Про 403: лимит расходуют не только успешные покупки, но и возвращённые, и
ещё не оплаченные открытые чекауты. То есть пользователь может увидеть 403
после того, как ему вернули деньги, — это ожидаемо, а не сбой.
Возврат плательщика¶
По завершении оплаты ПЦ редиректит браузер на наши страницы:
{PAYMENTS_PUBLIC_BASE_URL}/v1/payments/{payment_id}/return — операция одобрена
{PAYMENTS_PUBLIC_BASE_URL}/v1/payments/{payment_id}/fail — отказ
PAYMENTS_PUBLIC_BASE_URL — публичный адрес API (тот же хост, что и $BASE_URL).
Эти два маршрута намеренно отсутствуют в Swagger: они не часть JSON API, это
статические HTML-страницы для человека посреди редиректа. Ловите эти URL в
Custom Tab / SFSafariViewController и закрывайте окно оплаты.
Редирект — не подтверждение оплаты
Ни /return, ни /fail не проверяют статус: /return показывает «оплата
прошла» для любого payment_id. Единственный источник истины — GET
/v1/payments/{id}. Никогда не начисляйте и не показывайте успех по факту
попадания на /return.
Для СБП возврат в приложение не гарантирован: плательщик уходит в приложение своего банка, и вернётся ли он на нашу страницу — зависит от банка. Поэтому для СБП опирайтесь на стрим, а не на перехват URL.
Ускорить подтверждение¶
Редирект нельзя принять за исход, но он — точный сигнал момента: ПЦ в эту секунду уже знает результат. Скажите нам об этом, и мы спросим ПЦ сразу, не дожидаясь фоновой сверки:
Ответ — тот же объект, что у GET /v1/payments/{id}, уже с обновлённым статусом.
Вызывайте один раз при возвращении плательщика (перехват /return или /fail) и при
возврате приложения из фона, пока экран оплаты активен. Подтверждение занимает
секунду вместо минут.
Ручка аутентифицированная и только для владельца платежа — сам редирект подделывается кем угодно, и вешать на него запрос к ПЦ напрямую нельзя. Повторный вызов подряд безопасен: запросы к ПЦ чаще раза в несколько секунд не идут, ответ отдаётся из нашей базы. Терминальный платёж возвращается как есть.
Ошибки: 401 auth_required; 404 not_found — нет такого платежа или он чужой;
429 rate_limited — общий бюджет записей; 503 provider_unavailable — платежи
выключены. Не заменяет стрим: это ускоритель, а исход всё равно приходит стримом.
Статус¶
{
"id": "…", "status": "succeeded", "product_id": "…",
"amount_minor": 9900, "currency": 643, "credits": 100, "method": "card",
"created_at": "…", "paid_at": "…",
"receipt_url": "https://test.paygine.com/ofd/getReceipt?sector=…&operationId=…&signature=…"
}
Обратите внимание: payment_url в этом ответе нет.
Ошибки: 401 auth_required — без токена; 404 not_found — нет такого платежа
или он чужой (не 403, чтобы не подтверждать существование); 422
validation_error — id не UUID; 429 rate_limited — общий бюджет чтений.
status |
Что значит | Что показывать |
|---|---|---|
created |
ждём оплату | спиннер/«ожидаем подтверждение» |
succeeded |
деньги захвачены, кредиты зачислены | успех; перечитать /v1/credits/balance |
failed |
отказ (недостаточно средств и т.п.) | «не прошло», кнопка повторить |
expired |
не оплатили за время жизни заказа | предложить новый платёж |
refunded |
деньги возвращены плательщику | история |
Какие переходы возможны¶
Терминальны только succeeded и refunded. Всё остальное может ещё измениться:
failed → succeeded— плательщик повторил ввод карты на той же странице ПЦ, и вторая попытка прошла;expired → succeeded— редко, но возможно: наш срок истёк раньше, чем ПЦ подтвердил захват;succeeded → refunded— возврат, см. ниже.
Практический вывод: не закрывайте экран навсегда по failed или expired, пока
пользователь остаётся на странице оплаты. И будьте готовы увидеть refunded на
платеже, который раньше отдавал succeeded.
Ожидание итога¶
Ждите итог стримом, как у генераций и заявок:
Первое событие приходит сразу и несёт текущее состояние, дальше — по мере изменений:
data: {"id": "…", "status": "created", "paid_at": null}
data: {"id": "…", "status": "succeeded", "paid_at": "…"}
Стрим закрывается сам на succeeded и refunded. На failed и expired он
остаётся открытым — внутри времени жизни заказа плательщик может повторить попытку
на той же странице ПЦ, и она станет успешной.
Соединение почти наверняка оборвётся, пока плательщик находится на странице ПЦ или в приложении банка: приложение в это время свёрнуто. Это нормально — переподключайтесь при возврате в приложение, снапшот на подключении сразу отдаст актуальное состояние, ничего не теряется.
Поллинг GET /v1/payments/{id} остаётся запасным путём (например, если стрим не
удалось открыть) — раз в 2–3 секунды, пока экран оплаты активен. Учтите, что он делит
общий бюджет чтений с остальными ручками. Пуша о зачислении в фазе 1 нет.
Частый штатный случай: плательщик вернулся, а статус ещё created. Вебхук от ПЦ
асинхронный, зачисление делает воркер. Обычно это секунды. Показывайте ожидание, не
ошибку.
Ускорить это в ваших руках: POST /v1/payments/{id}/refresh при возврате плательщика
(см. выше) сокращает ожидание до секунды.
Сроки на случай, когда вебхук не дошёл, а refresh не вызвали (вебхук включают в
кабинете ПЦ, и до этого единственный фоновый канал — сверка):
- успех подтверждается сверкой в течение примерно 15 минут;
expiredпроставляется после того, как ПЦ сам объявит заказ просроченным — реалистично до 40 минут с момента создания, в редком случае застрявшего заказа до часа.
Поэтому: не показывайте ошибку по своему таймауту в одну минуту. Разумно — увести пользователя с экрана с текстом «проверим и начислим», и перечитать статус при следующем открытии приложения.
Возвраты¶
Статус refunded появляется не только по действию оператора:
- возврат из админки (карточка платежа → «Вернуть деньги»);
- возврат, сделанный в личном кабинете ПЦ;
- диспут/чарджбэк по карте — сервер следит за оплаченными платежами 45 дней и подхватит возврат сам.
Сервер отзывает остаток начисленных кредитов: потраченное не конфискуется, баланс в минус не уходит.
Если возвращённый платёж был периодом подписки, подписка при этом останавливается — подробности в «Подписках».
Отдельного эндпоинта истории платежей нет. Движения кредитов по покупкам видны в
GET /v1/credits/transactions с kind purchase и purchase_revoke.
Тестовый контур¶
Все вызовы идут на https://test.paygine.com; боевой контур включается только
конфигом сервера. Настоящие деньги там не двигаются.
Тестовые карты¶
Срок действия и CVV — любые, если в строке не указано иное.
| Карта | Что произойдёт | Статус платежа |
|---|---|---|
4986 2900 0000 0080 · 08/30 · CVV 899 |
оплата проходит | succeeded |
4000 0000 0009 1076 |
отказ: недостаточно средств | failed |
4000 0000 0009 1811 |
системная ошибка на стороне ПЦ | failed |
4000 0000 0000 1810 |
ПЦ не получил ответ эмитента | остаётся created, дорешается сверкой |
Последняя — самая полезная для проверки клиента: она моделирует случай, когда исход
неизвестен сразу. Платёж какое-то время остаётся created, и правильное поведение
приложения — показывать ожидание, а не ошибку.
СБП¶
Тестовый QR из приложения банка не оплатить: настоящий банк его не примет, и без оплаты
платёж примерно через полчаса станет expired. Оплату по СБП на тестовом контуре завершает
эмулятор ПЦ, и запускаем его мы: пришлите номер заказа, это параметр id в payment_url.
После запуска платёж становится succeeded за секунды, тем же путём, что и после настоящей
оплаты.
Как проверить свой клиент¶
- Возьмите пакет из
GET /v1/payments/products. POST /v1/payments→ откройтеpayment_url.- Введите одну из карт выше. 3-D Secure на тестовом контуре не спрашивает код.
- ПЦ вернёт вас на
/returnили/fail— закройте окно оплаты и вызовитеPOST /v1/payments/{id}/refresh. - Итог возьмите из
GET /v1/payments/{id}/stream.
Что стоит проверить обязательно, потому что в бою это встретится:
- успех —
succeeded, баланс вырос наcreditsпакета; - отказ —
failedи осмысленный текст на/fail; после него не закрывайте экран навсегда: тем жеpayment_urlможно попробовать другую карту, и платёж станетsucceeded; - неизвестный исход (карта
…1810) — статус какое-то времяcreated; - обрыв стрима — сверните приложение на минуту и вернитесь: переподключение к стриму должно отдать актуальное состояние снапшотом;
- потеря
payment_url— убейте приложение на странице оплаты. Ссылку взять неоткуда, нужен новый платёж; старый займёт слот доexpired.
Зачем refresh, если есть уведомления¶
Уведомления от ПЦ (вебхук) на тестовом контуре приходят, обычно за секунды. Но
доставку ПЦ не гарантирует: уведомление может задержаться или потеряться, и тогда
исход добирает фоновая сверка, до ~15 минут. POST /v1/payments/{id}/refresh при
возврате плательщика даёт подтверждение за секунду в любом случае, поэтому вызывайте
его всегда.