Перейти к содержанию

Оплата

Покупка кредитных пакетов за деньги. Платежи проводит процессинговый центр 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 и перечитать баланс. Даже если вебхук потерялся, фоновая сверка добьёт статус сама (о сроках — в разделе «Ожидание итога»).

Витрина

curl "$BASE_URL/v1/payments/products" -H "Authorization: Bearer $TOKEN"
{
  "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.

Ускорить подтверждение

Редирект нельзя принять за исход, но он — точный сигнал момента: ПЦ в эту секунду уже знает результат. Скажите нам об этом, и мы спросим ПЦ сразу, не дожидаясь фоновой сверки:

curl -X POST "$BASE_URL/v1/payments/{id}/refresh" -H "Authorization: Bearer $TOKEN"

Ответ — тот же объект, что у GET /v1/payments/{id}, уже с обновлённым статусом. Вызывайте один раз при возвращении плательщика (перехват /return или /fail) и при возврате приложения из фона, пока экран оплаты активен. Подтверждение занимает секунду вместо минут.

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

Ошибки: 401 auth_required; 404 not_found — нет такого платежа или он чужой; 429 rate_limited — общий бюджет записей; 503 provider_unavailable — платежи выключены. Не заменяет стрим: это ускоритель, а исход всё равно приходит стримом.

Статус

curl "$BASE_URL/v1/payments/{id}" -H "Authorization: Bearer $TOKEN"
{
  "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.

Ожидание итога

Ждите итог стримом, как у генераций и заявок:

curl -N "$BASE_URL/v1/payments/{id}/stream" -H "Authorization: Bearer $TOKEN"

Первое событие приходит сразу и несёт текущее состояние, дальше — по мере изменений:

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 за секунды, тем же путём, что и после настоящей оплаты.

Как проверить свой клиент

  1. Возьмите пакет из GET /v1/payments/products.
  2. POST /v1/payments → откройте payment_url.
  3. Введите одну из карт выше. 3-D Secure на тестовом контуре не спрашивает код.
  4. ПЦ вернёт вас на /return или /fail — закройте окно оплаты и вызовите POST /v1/payments/{id}/refresh.
  5. Итог возьмите из GET /v1/payments/{id}/stream.

Что стоит проверить обязательно, потому что в бою это встретится:

  • успех — succeeded, баланс вырос на credits пакета;
  • отказ — failed и осмысленный текст на /fail; после него не закрывайте экран навсегда: тем же payment_url можно попробовать другую карту, и платёж станет succeeded;
  • неизвестный исход (карта …1810) — статус какое-то время created;
  • обрыв стрима — сверните приложение на минуту и вернитесь: переподключение к стриму должно отдать актуальное состояние снапшотом;
  • потеря payment_url — убейте приложение на странице оплаты. Ссылку взять неоткуда, нужен новый платёж; старый займёт слот до expired.

Зачем refresh, если есть уведомления

Уведомления от ПЦ (вебхук) на тестовом контуре приходят, обычно за секунды. Но доставку ПЦ не гарантирует: уведомление может задержаться или потеряться, и тогда исход добирает фоновая сверка, до ~15 минут. POST /v1/payments/{id}/refresh при возврате плательщика даёт подтверждение за секунду в любом случае, поэтому вызывайте его всегда.