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

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

Что видит плательщик:

  1. Экран привязки и оплаты: название магазина, описание подписки, сумма и периодичность, дата следующего списания и как отменить подписку. СБП QR на компьютере или кнопка «Привязать счёт» на телефоне. Согласие плательщик ставит явной галочкой — кнопка «Перейти к оплате» активируется только после неё; открытие окна само по себе ничего не фиксирует.
  2. «Подписка оформлена» после оплаты либо «Не удалось» со ссылкой «Попробовать ещё раз», которая возвращает к экрану оплаты. После подтверждения в приложении банка плательщик возвращается в это же окно; если при создании подписки передан 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 и нажать «Привязать счёт» — открытие окна фиксирует условия подписки, а подтверждённое диспутами нарушение — основание отключить рекуррентные платежи на аккаунте.

В выписке банка клиент видит название магазина, зарегистрированное у банка.