Подписки¶
Регулярное пополнение кредитов: раз в период с карты плательщика списывается цена плана, и на баланс приходит пакет кредитов. Списаниями управляет наш сервер — приложение оформляет подписку и потом только показывает её состояние.
Кредиты плана живут один период. При продлении остаток сгорает и приходят кредиты нового периода; когда подписка закрывается, остаток сгорает совсем. Кредиты разовых пакетов, приветственные и заработанные не сгорают никогда. Сколько сгорит и когда, отдаёт
GET /v1/credits/balance:plan_creditsиplan_expires_at.
Флоу целиком¶
GET /v1/subscriptions/plans → витрина планов
POST /v1/subscriptions → 201 {id, status: pending, payment_url, payment_id}
открыть payment_url → страница оплаты ПЦ (первый период)
POST /v1/payments/{payment_id}/refresh → как в обычной оплате
GET /v1/payments/{payment_id}/stream → исход первого списания
GET /v1/subscriptions/{id} → status стал active
Первый период оплачивается обычным платежом — той же страницей ПЦ, тем же
стримом, теми же статусами, что и разовый пакет (см. Оплата). Ничего
нового в этом шаге нет: подписка становится active ровно тогда, когда этот платёж
становится succeeded.
Все последующие периоды списываются сервером с сохранённой карты, без участия приложения и без открытия каких-либо страниц.
Витрина планов¶
{
"items": [
{
"id": "…", "code": "plan_500", "title": "500 кредитов в месяц",
"price_minor": 49900, "credits": 500, "badge": null,
"billing_period": "monthly"
}
],
"total": 1, "limit": 1, "offset": 0
}
billing_period — monthly или yearly. price_minor — цена одного периода в
копейках, credits — сколько кредитов приходит каждый период.
Планы и разовые пакеты живут на разных ручках и никогда не смешиваются:
GET /v1/payments/products отдаёт только разовые пакеты, эта — только планы.
Оформление¶
curl -X POST "$BASE_URL/v1/subscriptions" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"product_id": "…", "method": "card"}'
{
"id": "…", "status": "pending",
"product_id": "…", "amount_minor": 49900, "currency": 643, "credits": 500,
"billing_period": "monthly",
"current_period_end": null, "next_charge_at": null, "cancelled_at": null,
"created_at": "…",
"payment_id": "…", "payment_url": "https://test.paygine.com/webapi/Purchase?…"
}
Дальше — как в обычной оплате: открыть payment_url, поймать возврат плательщика,
вызвать POST /v1/payments/{payment_id}/refresh и дождаться исхода
стримом платежа.
Ошибки: 404 not_found — плана нет или он отключён (в том числе если передан id
разового пакета); 422 validation_error — method не card; 409 already_subscribed —
подписка на этот план уже есть, в том числе ещё не оплаченная; 403 forbidden — план
продаётся ограниченное число раз на аккаунт, и лимит исчерпан; 429 rate_limited — слишком
много незавершённых оплат, как у разового пакета; 503 provider_unavailable — платежи
выключены.
Повтор оформления. Без заголовка повтор того же запроса упирается в
409 already_subscribed, и payment_url из потерянного ответа уже не получить. С
заголовком Idempotency-Key повтор с тем же ключом и тем же телом возвращает ту же
подписку с тем же payment_id и payment_url. Ответы 409 request_in_progress и
409 idempotency_key_reused значат то же, что у разовой оплаты.
Условия фиксируются при оформлении. amount_minor и credits в объекте подписки — то,
что будет списываться и начисляться каждый период. Если цена плана на витрине потом
изменится, действующая подписка продлевается на прежних условиях; новая цена действует для
тех, кто оформляет заново.
Состояние подписки¶
curl "$BASE_URL/v1/subscriptions" -H "Authorization: Bearer $TOKEN" # все свои
curl "$BASE_URL/v1/subscriptions/{id}" -H "Authorization: Bearer $TOKEN" # одна
status |
Что значит | Что показывать |
|---|---|---|
pending |
оформлена, первый период ещё не оплачен | «ожидаем оплату», вести на payment_url |
active |
оплачена, продлевается автоматически | дату следующего списания |
past_due |
списание отклонено, идут повторы; кредиты плана пока доступны | попросить проверить карту |
cancelled |
отменена пользователем или остановлена возвратом денег; оплаченный период ещё идёт | «доступ до current_period_end» и предложить оформить заново |
expired |
подписка закрыта: оплаченный период закончился после отмены, повторы кончились, первая оплата так и не прошла, продлить нечем или оплачен другой план | предложить оформить заново |
current_period_end— до какого момента период оплачен;next_charge_at— когда сервер попробует списать в следующий раз.nullозначает, что списаний больше не будет (отменена, закрыта или ещё ждёт первой оплаты).
Отказ не закрывает подписку сразу. Сервер повторяет списание на 1-й, 3-й и 5-й
день; всё это время статус past_due, а кредиты плана остаются доступны. Если ни одна
попытка не прошла, подписка переходит в expired, и остаток кредитов плана сгорает.
О каждом отказе плательщик получает push — на все свои устройства, зарегистрированные
через POST /v1/devices. В data приходит subscription_id и status (past_due или
expired), так что уведомление можно открыть сразу на экране подписки. Отдельного стрима
у подписки нет: состояние читается из GET /v1/subscriptions/{id}.
Перед концом периода приходит напоминание, тем же push. В data - subscription_id,
status: "period_ending" и days_left ("3" или "1"):
- за 3 дня до автосписания - что и сколько спишется; если кредиты плана ещё остались, там же сказано, что они сгорят;
- за 1 день - только если кредиты плана остались;
- у отменённой подписки - за 3 дня и за 1 день, и только если есть чему сгореть.
Возврат денег за период останавливает подписку. Если платёж возвращён (оператором или
по итогам спора), подписка переходит в cancelled, next_charge_at становится null, а
current_period_end сдвигается на момент возврата — период больше не оплачен. Начисленные
за него кредиты списываются в пределах остатка на балансе, начиная с кредитов плана.
Один план на аккаунт¶
Оформить другой план можно, не отменяя текущий: POST /v1/subscriptions с новым
product_id. Пока новый план не оплачен, текущий работает как работал, так что брошенная
страница оплаты ничего не отнимает. В момент оплаты новый план заменяет прежний: прежняя
подписка переходит в expired, остаток её кредитов сгорает, приходят кредиты нового плана.
Остаток не переносится. Повторное оформление того же плана при живой подписке по-прежнему
отвечает 409 already_subscribed.
Отмена¶
curl -X POST "$BASE_URL/v1/subscriptions/{id}/cancel" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"reason": "too_expensive", "comment": "трачу меньше, чем приходит"}'
Отвечает тем же объектом со status: cancelled. Списаний больше не будет, а оплаченный
период остаётся: кредиты плана доступны до current_period_end и сгорают в этот
момент. Это и есть «доступ до конца периода».
Причина обязательна. reason - одно из значений:
reason |
Подпись в приложении |
|---|---|
too_expensive |
Дорого |
rarely_use |
Редко пользуюсь |
quality |
Не устроило качество |
missing_effects |
Нет нужных эффектов |
technical |
Технические проблемы |
other |
Другое |
comment - свободный текст до 500 символов, необязателен; для other стоит дать поле
ввода. Запрос без reason или с другим значением - 422 validation_error, подписка при
этом не отменяется.
Повторная отмена ничего не меняет и возвращает тот же объект (не ошибка), причина остаётся
той, что была названа первой. Отменить чужую подписку нельзя — 404, как и при чтении.
Чего пока нет¶
- СБП-подписок: привязка по СБП требует отдельного согласия плательщика в
приложении банка.
methodпринимает толькоcard, наsbpоформление отвечает422. - Переноса остатка при смене плана: новый план заменяет прежний, остаток сгорает (см. «Один план на аккаунт»).
- Чеков продлений в API: каждый период - отдельный платёж со своим чеком (54-ФЗ),
но ссылка на чек (
receipt_url) доступна приложению только у платежа первого периода: истории платежей в API пока нет.