В B2B SaaS деньги — это не кнопка «купить», а цепочка: счёт на компанию, оплата с корпоративного счёта или карты сотрудника, продление доступа на следующий период и сверка в конце месяца. Платёжный сервис закрывает середину этой цепочки — приём оплаты и подтверждённое событие о ней. Всё, что вокруг: тарифы, места в команде, даты окончания периода и документы — живёт в вашем продукте, и спроектировать это надо один раз и аккуратно.
Ниже — рабочая схема: что хранить в счёте, как считать право доступа, что делать с просрочкой и где проходит граница ответственности.
Чем оплата в B2B отличается от розничной
Розничный сценарий короткий: человек нажал, заплатил, получил файл. В B2B между решением и деньгами стоит процедура, и она ломает наивную интеграцию.
- Платит не тот, кто пользуется. Карту достаёт бухгалтер или руководитель, а работать в сервисе будут пятеро других сотрудников.
- Между счётом и оплатой проходят дни. Согласование в компании — норма, поэтому счёт должен пережить неделю и остаться валидным.
- Оплата продлевает, а не открывает. Клиент уже работает, и задача платежа — сдвинуть дату окончания, а не создать новый аккаунт.
- Нужны документы. Закрывающие бумаги и корректная сумма важнее скорости.
Отсюда главное архитектурное решение: аккаунт клиента, счёт и платёж — три разные сущности. Аккаунт живёт годами, счёт относится к одному периоду, платёж относится к одному счёту.
Счёт со снимком тарифа: что фиксировать при выставлении
Счёт — это не сумма, а замороженное состояние сделки. В момент выставления запишите в него всё, что может измениться позже:
| Поле | Зачем нужно |
|---|---|
| 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 из активного счёта. Захотел клиент добавить двух человек в середине периода — выставьте отдельный счёт на разницу за оставшиеся дни, а не пересчитывайте старый. Так история платежей остаётся читаемой при разборе спора.
Продление и просрочка: что делать, когда деньги опоздали
Корпоративный клиент почти всегда платит позже срока — это не злой умысел, а внутренние регламенты. Заложите это в продукт заранее.
- За 10 дней до конца периода выставьте счёт и отправьте ссылку тому, кто в аккаунте отмечен как плательщик.
- За 3 дня напомните и продублируйте админу аккаунта — часто именно он двигает согласование.
- В день окончания включите grace-период: доступ остаётся, но в интерфейсе появляется баннер о задолженности.
- После 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; для клиентов из других стран нужен отдельный маршрут. - Тарифы, места и пропорциональный пересчёт — ваша логика. Сервис проводит платёж на переданную сумму, считаете её вы.
- Автопродление настраиваете вы. В описанной схеме каждый период — это новый счёт и новая оплата, поэтому напоминания и планировщик пишете на своей стороне.
- Документы и налоги на продавце. Закрывающие бумаги, статус и отчётность — зона ответственности вашей компании.
- Категорию и условия подтверждает модерация. Опишите продукт и модель продаж честно — так проверка проходит быстрее.
Если продукт ещё не дорос до счетов и периодов, начните проще — со страницы про платежи для стартапа, а биллинг добавьте, когда появятся первые продления.