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

Подписки

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

Кредиты плана живут один период. При продлении остаток сгорает и приходят кредиты нового периода; когда подписка закрывается, остаток сгорает совсем. Кредиты разовых пакетов, приветственные и заработанные не сгорают никогда. Сколько сгорит и когда, отдаёт 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.

Все последующие периоды списываются сервером с сохранённой карты, без участия приложения и без открытия каких-либо страниц.

Витрина планов

curl "$BASE_URL/v1/subscriptions/plans" -H "Authorization: Bearer $TOKEN"
{
  "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 пока нет.