Привязка через окно X-Hub
На части аккаунтов подписка привязывается не в стороннем окне, а в нашем платёжном окне привязки. Контракт API тот же: POST /subscriptions, GET, POST /:id/charges, PATCH /:id, POST /:id/cancel, те же вебхуки. Меняется то, что видит плательщик, и правила вокруг списаний. Какая схема у твоего аккаунта, скажет команда X-Hub при подключении.
Старые привязки не переносятся
Банк требует нового согласия плательщика. Для каждого клиента создай новую подписку и отправь ему новый binding_url. Старые подписки на прежней схеме отмени обычным POST /subscriptions/:id/cancel.
Ссылка привязки
POST /subscriptions сразу отдаёт binding_url на окно X-Hub вида https://…/p/s/<id подписки>, поле binding_url_pending всегда false. В момент создания подписки деньги не двигаются и банк не вызывается: всё начинается, когда плательщик откроет ссылку. Ссылку можно отправить в мессенджер и открыть через несколько часов.
Что видит плательщик:
- Экран привязки и оплаты: название магазина, описание подписки, сумма и периодичность, дата следующего списания и как отменить подписку. СБП QR на компьютере или кнопка «Привязать счёт» на телефоне. Согласие плательщик ставит явной галочкой — кнопка «Перейти к оплате» активируется только после неё; открытие окна само по себе ничего не фиксирует.
- «Подписка оформлена» после оплаты либо «Не удалось» со ссылкой «Попробовать ещё раз», которая возвращает к экрану оплаты. После подтверждения в приложении банка плательщик возвращается в это же окно; если при создании подписки передан
success_url, над статусом появляется кнопка «В магазин» с автовозвратом через 5 секунд.
В окне привязки плательщик видит конкретные даты: следующее списание — «не раньше {дата}», конец оплаченного периода — минус день от него. Дата считается от даты привязки и по московскому времени. Если после создания подписки ты поменял сумму через PATCH до привязки, плательщик увидит на экране уже новые условия; после привязки изменение условий — тема следующего пункта.
Периодичность: допустимые значения
interval + interval_count задаются при создании и потом не меняются:
interval |
interval_count |
Пресеты |
|---|---|---|
day |
1…365 | day×1 — ежедневно, day×3 — раз в 3 дня |
week |
1…52 | week×1 — еженедельно, week×2 — раз в 2 недели |
month |
1…12 | month×1 — ежемесячно |
Границы — жёсткие: за пределами вернётся 400 VALIDATION_ERROR с указанием поля. Следующее списание считается как «дата привязки + interval_count × interval» с клэмпом конца месяца (привязка 31 января при month×1 → следующее списание 28 февраля). Ровно эти же даты и числа бюджета повторов (ниже) напечатаны в окне привязки — плательщик и мерчант видят одни и те же условия.
Первое списание — это сама привязка
Оплата в окне списывает первый период на сумму подписки amount_rub. Дальше всё как раньше:
payment.status_changedсоstatus: "paid",payment_method: "recurring",subscription_idиperiod_ref;subscription.status_changed:pending_binding→active.
Порядок этих двух событий не гарантирован. Первый период уже оплачен, поэтому своё POST /:id/charges планируй на следующий период (см. предупреждение о двойном списании).
Окно следующего списания: «не раньше»
Твои списания (POST /subscriptions/:id/charges) разрешены, начиная с даты следующего списания из согласия — то есть «якорь» (дата последнего успешного платежа) плюс период подписки. Раньше — 409 CHARGE_BEFORE_WINDOW, а в теле ошибки поле details.next_allowed_at (ISO, московское время): момент, когда списание пройдёт. Планируй свой крон на него, а не на собственный пересчёт периода.
- Пауза не нужна и после неуспешной попытки: пока не исчерпан бюджет повторов, повторять можно в любой момент разрешённого периода (см. ниже).
- Дата привязки сдвигается только деньгами: каждая успешная оплата (в том числе твоё списание) становится новым «якорем», и окно следующего списания отсчитывается от неё. Неуспешные попытки якорь не двигают.
Списания и повторы после отказа
Следующие списания — POST /subscriptions/:id/charges, как раньше. Итог приходит вебхуком payment.status_changed, обычно за секунды.
Если списание не прошло (failed), в том же периоде его можно повторить — ровно в том бюджете, который плательщик видел в окне привязки («магазин сможет повторить его до N раз в течение H часов»):
| Периодичность подписки | Попыток за период | Окно от 1-й попытки |
|---|---|---|
day×1 |
2 | 3 часов |
day×3 |
3 | 6 часов |
week×1 |
4 | 12 часов |
week×2 |
4 | 24 часов |
| остальные (окно от недели и больше) | 4 | 20 часов |
Правила бюджета:
- вышло окно с первой попытки или исчерпано число попыток — новые списания в этом периоде отклоняются
409 RECURRING_CHARGE_EXHAUSTEDсdetails.next_allowed_at= начало следующего периода. Об этом же приходит вебхукsubscription.charge_exhausted(один на период); - попытка, которая ещё обрабатывается (платёж
pending), блокирует новые:POST /chargesвернёт200и ту же последнюю попытку, новой не создаст. Банк в этот момент не вызывается; period_refу всех попыток периода одинаковый — по нему (и поGET /:id/charges) видно, сколько попыток период уже израсходовал.
rebind_required: true — повторять бесполезно
Если банк перестал принимать токен плательщика (он отключил подписку в приложении банка, сменил банк или отозвал согласие), подписка переходит в paused с rebind_required: true и приходит subscription.status_changed. Списания по ней не пройдут (409 CHARGE_MANDATE_INACTIVE). Создай клиенту новую подписку и отправь новую ссылку, старую отмени.
Уведомляй плательщика о повторах
Повтор списания виден плательщику как отдельный дебет. Если клиент не ждал его, он диспутит. Перед каждой повторной попыткой (и при исчерпании бюджета — как сигнале «списание не случится до next_allowed_at») отправляй плательщику своё уведомление: письмо, пуш или сообщение в канале, где он оформлял подписку. Магазин, который держит плательщика в курсе, почти не получает диспутов «я не соглашался».
Изменение условий после привязки
Плательщик согласился на конкретные условия, и мандат фиксирует сумму. PATCH /subscriptions/:id работает, только пока подписка active или pending_binding; у paused, cancelled и expired он отклоняется 409 INVALID_STATE — мёртвый мандат не «готовят» к новой сумме. Итоговая плановая сумма не может быть выше cap_amount_rub (400 INVALID_AMOUNT).
Условия выше согласованных (сумма, периодичность) — это новая подписка и новое согласие плательщика, не PATCH.
Отмена
POST /subscriptions/:id/cancel работает как раньше и отвечает cancelled. Кроме закрытия подписки у нас мы отключаем списания и на стороне банка. Если банк в эту секунду недоступен, подписка всё равно отменена, и новые списания по ней невозможны (409 CHARGE_MANDATE_INACTIVE). Отключение в банке мы повторим сами, делать ничего не нужно. Повторный POST /cancel по отменённой подписке, как и раньше, вернёт 409 INVALID_STATE.
Возвраты
Любое списание по подписке, включая первое, возвращается обычным API возвратов. Правила те же, что у разовых платежей. Комиссия за успешный платёж при возврате не возвращается.
Требования к мерчанту
Диспуты «я не соглашался» ложатся на тебя. Чтобы их не было:
- Одна подписка на клиента. Переиспользуй
Idempotency-Keyи не создавай подписку на каждое открытие формы оплаты. - Явное согласие у себя до выдачи ссылки. Покажи клиенту сумму, периодичность и как отменить. Рекомендованный текст: «Оформляя подписку, я соглашаюсь на регулярные списания {сумма} ₽ раз в {период}. Отменить подписку можно в любой момент в {где}».
- Не прячь подписку за кнопками «тестовая», «пробная» или «проверить оплату» без суммы и периодичности.
- Уведомляй о списаниях и повторах — до списания и при каждой неуспешной попытке, см. выше.
- Не перерисовывай ссылку в свой QR и не открывай оплату в обход окна: плательщик обязан открыть окно X-Hub и нажать «Привязать счёт» — открытие окна фиксирует условия подписки, а подтверждённое диспутами нарушение — основание отключить рекуррентные платежи на аккаунте.
В выписке банка клиент видит название магазина, зарегистрированное у банка.