Решение RollyPay

Приём платежей для B2B SaaS

Счёт со слепком тарифа, отдельная запись о праве доступа и продление строго по подтверждённому событию payment.paid. Приём оплаты и подписанный колбэк — на стороне сервиса, тарифы, места и документы — на вашей.

Обсудить мой сценарий

В B2B SaaS деньги — это не кнопка «купить», а цепочка: счёт на компанию, оплата с корпоративного счёта или карты сотрудника, продление доступа на следующий период и сверка в конце месяца. Платёжный сервис закрывает середину этой цепочки — приём оплаты и подтверждённое событие о ней. Всё, что вокруг: тарифы, места в команде, даты окончания периода и документы — живёт в вашем продукте, и спроектировать это надо один раз и аккуратно.

Ниже — рабочая схема: что хранить в счёте, как считать право доступа, что делать с просрочкой и где проходит граница ответственности.

Чем оплата в B2B отличается от розничной

Розничный сценарий короткий: человек нажал, заплатил, получил файл. В B2B между решением и деньгами стоит процедура, и она ломает наивную интеграцию.

  • Платит не тот, кто пользуется. Карту достаёт бухгалтер или руководитель, а работать в сервисе будут пятеро других сотрудников.
  • Между счётом и оплатой проходят дни. Согласование в компании — норма, поэтому счёт должен пережить неделю и остаться валидным.
  • Оплата продлевает, а не открывает. Клиент уже работает, и задача платежа — сдвинуть дату окончания, а не создать новый аккаунт.
  • Нужны документы. Закрывающие бумаги и корректная сумма важнее скорости.

Отсюда главное архитектурное решение: аккаунт клиента, счёт и платёж — три разные сущности. Аккаунт живёт годами, счёт относится к одному периоду, платёж относится к одному счёту.

Схема биллинга SaaS: счёт, оплата, подтверждение и продление доступа
Схема. Счёт связывает аккаунт клиента и конкретный платёж, а дата окончания доступа сдвигается только после подтверждённой оплаты.

Счёт со снимком тарифа: что фиксировать при выставлении

Счёт — это не сумма, а замороженное состояние сделки. В момент выставления запишите в него всё, что может измениться позже:

ПолеЗачем нужно
account_idЧей это счёт: компания, а не конкретный пользователь
plan_snapshotНазвание тарифа, цена, что входит — на момент выставления
seatsСколько мест оплачено, чтобы потом не спорить
period_start / period_endЗа какой отрезок берутся деньги
amount, statusСумма и одно из состояний: draft, sent, paid, void
order_idТот же идентификатор уходит в платёж и возвращается в колбэке

Если завтра вы поднимете цену Pro с 12 000 до 15 000 ₽, счёт, выставленный вчера, обязан остаться на старой сумме. Именно поэтому в счёте лежит слепок тарифа, а не ссылка на строку в прайсе.

Один счёт — один order_id. Создание платежа идемпотентно по нему, так что повторный запрос при обрыве связи не породит второй платёж.

Право доступа: места, лимиты и дата окончания

Не выдавайте доступ «фактом оплаты». Держите отдельную запись о праве пользоваться продуктом — с датой окончания и количеством мест. Оплата её продлевает, отсутствие оплаты — нет.

subscription
  account_id     uuid
  plan           text
  seats          int
  active_until   timestamp   -- единственный источник правды о доступе
  grace_until    timestamp   -- сколько ещё пускаем после просрочки
  last_invoice   uuid

Проверка на входе в продукт становится одной строчкой: now() < active_until. При оплате счёта active_until сдвигается на длину периода — от даты окончания, а не от даты платежа. Клиент, заплативший за три дня до конца месяца, не теряет эти три дня.

Места считайте по seats из активного счёта. Захотел клиент добавить двух человек в середине периода — выставьте отдельный счёт на разницу за оставшиеся дни, а не пересчитывайте старый. Так история платежей остаётся читаемой при разборе спора.

Продление и просрочка: что делать, когда деньги опоздали

Корпоративный клиент почти всегда платит позже срока — это не злой умысел, а внутренние регламенты. Заложите это в продукт заранее.

  1. За 10 дней до конца периода выставьте счёт и отправьте ссылку тому, кто в аккаунте отмечен как плательщик.
  2. За 3 дня напомните и продублируйте админу аккаунта — часто именно он двигает согласование.
  3. В день окончания включите grace-период: доступ остаётся, но в интерфейсе появляется баннер о задолженности.
  4. После grace ограничивайте по частям: сначала выключайте создание нового, потом приглашения новых пользователей, и только в последнюю очередь — чтение данных.

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

Интеграция: платёж, колбэк, идемпотентность

Счёт превращается в оплату одним запросом POST /api/v1/payments с заголовками X-API-Key и X-Nonce — новый UUID на каждый вызов, он живёт 10 минут. В теле передайте amount строкой, payment_currency: "RUB", order_id счёта и metadata с внутренними полями вроде account_id и номера периода: этот объект вернётся в колбэке и избавит вас от лишнего похода в базу.

В ответе придёт pay_url — его вставляйте в письмо со счётом. Ссылка живёт до expires_at, и для B2B это важно: если согласование затянулось и срок истёк, выставьте новый платёж по тому же счёту, а не просите клиента «попробовать ещё раз».

Продление включайте только по колбэку. Заголовок X-Signature — это HMAC-SHA256 от X-Timestamp, точки и сырого тела запроса; проверяйте подпись до разбора JSON и сравнивайте constant-time. События — payment.paid, payment.canceled, payment.expired. Обработчик должен быть безопасен к повторам: если счёт уже paid, второй колбэк не двигает active_until ещё раз. Разбор этого механизма есть в статье про идемпотентность платёжных запросов.

Сверка и закрытие месяца

Раз в месяц бухгалтерия спросит, сходятся ли поступления с выставленным. Чтобы ответ занимал минуты, храните в каждом счёте payment_id и время последнего события — тогда сверка становится обычным отчётом.

  • счета в статусе sent старше 30 дней — кандидаты на повторный контакт;
  • оплаты без счёта — почти всегда ручной перевод, который нужно привязать;
  • расхождение суммы — признак частичной оплаты или изменения тарифа задним числом;
  • события payment.expired по активным клиентам — сигнал, что ссылка протухла раньше, чем прошло согласование.

Подробнее методика описана в материале о сверке платежей для B2B SaaS. Практика простая: если сверка руками занимает больше получаса, значит, где-то в модели не хватает связи между счётом и платежом.

Чего платёжный сервис не делает за вас

Здесь стоит быть точным, чтобы не строить планы на несуществующее.

  • Расчёты пока в рублях. Поле payment_currency принимает RUB; для клиентов из других стран нужен отдельный маршрут.
  • Тарифы, места и пропорциональный пересчёт — ваша логика. Сервис проводит платёж на переданную сумму, считаете её вы.
  • Автопродление настраиваете вы. В описанной схеме каждый период — это новый счёт и новая оплата, поэтому напоминания и планировщик пишете на своей стороне.
  • Документы и налоги на продавце. Закрывающие бумаги, статус и отчётность — зона ответственности вашей компании.
  • Категорию и условия подтверждает модерация. Опишите продукт и модель продаж честно — так проверка проходит быстрее.

Если продукт ещё не дорос до счетов и периодов, начните проще — со страницы про платежи для стартапа, а биллинг добавьте, когда появятся первые продления.

Частые вопросы

Почему счёт нельзя привязывать к строке в прайсе?

Потому что прайс меняется, а выставленный счёт менять нельзя. Записывайте в счёт слепок тарифа: название, цену, число мест и границы периода на момент выставления. Тогда повышение цены завтра не затронет счёт, отправленный клиенту вчера, и при споре видно, за что именно человек платил.

Как правильно продлевать доступ после оплаты?

Держите отдельную запись о подписке с полем active_until — это единственный источник правды о доступе. При оплате счёта сдвигайте дату от конца прошлого периода, а не от даты платежа, иначе клиент теряет оплаченные дни. Проверка на входе в продукт сводится к сравнению текущего времени с active_until.

Что делать, если корпоративный клиент оплатил счёт с опозданием?

Заложите grace-период: доступ остаётся ещё несколько дней после окончания оплаченного срока, а в интерфейсе появляется предупреждение о задолженности. Ограничивайте функции по частям — сначала создание нового, потом приглашение пользователей, чтение данных отключайте последним. Если ссылка на оплату истекла по expires_at, выставьте новый платёж по тому же счёту.

Как связать платёж с внутренним биллингом?

Передавайте в запросе создания платежа свой order_id счёта и объект metadata с внутренними полями вроде account_id и номера периода — он вернётся в колбэке. Создание платежа идемпотентно по order_id, поэтому повтор при обрыве связи не породит второй платёж. В счёте храните payment_id и время последнего события, чтобы месячная сверка занимала минуты.

Что остаётся на стороне SaaS, а не платёжного сервиса?

Тарифы, число мест, пропорциональный пересчёт, напоминания о продлении и планировщик счетов пишете вы. Закрывающие документы, налоги и статус продавца — зона ответственности вашей компании. Сервис принимает оплату на переданную сумму и присылает подписанное событие о результате; расчёты сейчас идут в рублях, поэтому для клиентов из других стран нужен отдельный маршрут.