Skip to content
Назад в блог

Монетизация SaaS: Как безболезненно настроить Stripe, подписки и биллинг

4 мин. чтения
Монетизация SaaS: Как безболезненно настроить Stripe, подписки и биллинг

Интеграция рекуррентных платежей в SaaS-продукты обманчиво проста на словах. Но если разовое списание средств с карты реализуется быстро, то управление жизненным циклом подписок вскрывает десятки подводных камней: отслеживание истечения срока действия карт, интервалы повторных попыток списания, апгрейды и даунгрейды тарифных планов, льготные периоды (grace periods) и обработка вебхуков.

Если вебхук от платёжной системы обработан некорректно или не в том порядке, вы рискуете заблокировать платящих клиентов либо предоставить бесплатный доступ тем, кто уже отменил подписку.

В этом практическом руководстве мы настроим отказоустойчивый обработчик Stripe Webhooks с проверкой подписи на Node.js/TypeScript и опишем процесс синхронизации данных.

1. Анатомия жизненного цикла подписки

Диаграмма состояний подписки Stripe и обработка вебхуков

Ваша база данных должна точно отслеживать статус подписки клиента. Stripe использует несколько ключевых статусов, которые вы должны синхронизировать со своей БД:

               +--------------------------------------+
               |               trialing               |
               +------------------+-------------------+
                                  | (Триал завершён или оформлена оплата)
                                  v
+------------------->  +----------------------+
|                      |        active        | <------------------+
|                      +----------+-----------+                    |
| (Карта обновлена,               |                                |
|  списание успешно)              | (Списание не прошло, Stripe        | (Продление
|                                 |  повторяет попытки)                |  до окончания
|                                 v                                |  периода)
|                      +----------+-----------+                    |
+---------------------+|       past_due       |                    |
                       +----------+-----------+                    |
                                  | (Все попытки списания провалились) |
                                  v                                |
                       +----------+-----------+                    |
                       |       canceled       | -------------------+
                       +----------------------+
  • trialing: Бесплатный ознакомительный период. Доступ открыт.
  • active: Платёж прошёл успешно. Полный доступ.
  • past_due: Очередной платёж не прошёл. Не блокируйте пользователя сразу. Переведите аккаунт в предупредительный статус (льготный период от 3 до 7 дней), отключите ресурсоёмкие API-методы и отправьте напоминание об обновлении платёжных данных.
  • canceled: Подписка полностью аннулирована. Доступ закрыт. Для возобновления клиент должен заново привязать карту.

2. Реализация обработчика Stripe Webhooks с верификацией подписи

Stripe отправляет асинхронные уведомления об изменениях статусов через HTTP POST запросы. Поскольку любой злоумышленник может отправить поддельный запрос на ваш эндпоинт /api/billing/webhook, вы обязаны валидировать криптографическую подпись Stripe в заголовке stripe-signature.

Пример реализации обработчика на Node.js/TypeScript и Express:

import express from 'express';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

const app = express();
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;

// ВАЖНО: Проверка подписи Stripe требует сырого буфера тела запроса (raw body).
// Не применяйте bodyParser.json() к этому маршруту до проверки подписи.
app.post(
  '/api/billing/webhook',
  express.raw({ type: 'application/json' }),
  async (req: express.Request, res: express.Response) => {
    const sig = req.headers['stripe-signature'];
    
    if (!sig) {
      return res.status(400).send('Отсутствует заголовок stripe-signature.');
    }

    let event: Stripe.Event;

    try {
      // Проверяем подпись вебхука, используя секрет из кабинета Stripe
      event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
    } catch (err: any) {
      console.error(`[Security Warning] Ошибка проверки подписи: ${err.message}`);
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    try {
      switch (event.type) {
        case 'customer.subscription.created':
        case 'customer.subscription.updated': {
          const subscription = event.data.object as Stripe.Subscription;
          await handleSubscriptionUpdate(subscription);
          break;
        }
        case 'customer.subscription.deleted': {
          const subscription = event.data.object as Stripe.Subscription;
          await handleSubscriptionDeletion(subscription);
          break;
        }
        case 'invoice.payment_failed': {
          const invoice = event.data.object as Stripe.Invoice;
          await handlePaymentFailure(invoice);
          break;
        }
        default:
          console.log(`[Stripe Webhook] Пропущено событие: ${event.type}`);
      }

      res.json({ received: true });
    } catch (err: any) {
      console.error(`[Webhook Process Error] Ошибка обработки события: ${err.message}`);
      res.status(500).send('Internal Server Error');
    }
  }
);

async function handleSubscriptionUpdate(sub: Stripe.Subscription) {
  const tenantId = sub.metadata.tenantId;
  if (!tenantId) {
    throw new Error(`В метаданных подписки ${sub.id} отсутствует ID клиента (tenantId).`);
  }

  const status = sub.status; // 'active', 'trialing', 'past_due' и др.
  const priceId = sub.items.data[0].price.id;
  const currentPeriodEnd = new Date(sub.current_period_end * 1000);

  // Синхронизируем статус в нашей базе данных
  await db.query(
    `UPDATE tenants 
     SET subscription_status = $1, stripe_price_id = $2, subscription_period_end = $3
     WHERE id = $4`,
    [status, priceId, currentPeriodEnd, tenantId]
  );

  console.log(`[Billing Sync] Статус клиента ${tenantId} обновлён на: ${status}`);
}

async function handleSubscriptionDeletion(sub: Stripe.Subscription) {
  const tenantId = sub.metadata.tenantId;
  if (!tenantId) return;

  await db.query(
    `UPDATE tenants 
     SET subscription_status = 'canceled', stripe_price_id = NULL
     WHERE id = $1`,
    [tenantId]
  );

  console.log(`[Billing Sync] Подписка клиента ${tenantId} аннулирована.`);
}

async function handlePaymentFailure(invoice: Stripe.Invoice) {
  const customerId = invoice.customer as string;
  // Находим пользователя по customerId и отправляем предупреждающее письмо
  console.warn(`[Billing Alert] Ошибка оплаты счёта для клиента ${customerId}`);
}

3. Рекомендации по интеграции биллинга в SaaS

  1. Stripe Checkout и Customer Portal спасают недели работы: Не тратьте ресурсы на создание дашбордов привязки карт, просмотра истории инвойсов и отмены услуг. Готовые решения Stripe безопасны, соответствуют PCI DSS и переведены на десятки языков.
  2. Используйте Metadata: Всегда передавайте ID сущностей вашей базы данных в поле metadata при создании платёжных сессий. Это единственный надёжный способ сопоставить событие вебхука с конкретной строкой в вашей БД.
  3. Защита от дублирования (Идемпотентность): Вебхуки могут приходить повторно. Убедитесь, что ваш обработчик проверяет, не был ли статус уже записан, чтобы исключить лишние операции отправки писем или пересчёта лимитов.

Например, в проекте TeleGo.io мы внедрили именно эту архитектуру верификации вебхуков для безопасного управления подписками. Мы также совместили её с микротранзакциями Telegram Stars, о чём вы можете прочитать в моей статье «Разработка SaaS внутри Telegram Web Apps».

Источники и документация

Похожие инструменты

Профессиональная платёжная архитектура защищает доходы вашего SaaS-бизнеса и гарантирует высокий уровень лояльности платящих клиентов. Чтобы узнать, как биллинг встраивается в общий цикл быстрого запуска стартапа, ознакомьтесь с моим руководством «Как собрать B2B SaaS за 30 дней».

Если вы планируете настроить приём платежей на своём проекте или провести рефакторинг текущей платёжной системы, изучите мою услугу разработки SaaS-платформ или запишитесь на консультацию, чтобы разработать надёжный биллинг.

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

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

Редирект на success-страницу не гарантирует, что деньги списаны: пользователь мог закрыть вкладку, а платёж — не пройти антифрод. Единственный источник истины — вебхуки Stripe: доступ выдаётся по событию checkout.session.completed, а продлевается по invoice.paid.

Что делать, если вебхук от Stripe не дошёл?

Stripe автоматически повторяет доставку событий до трёх суток, поэтому обработчик обязан быть идемпотентным — повторное событие не должно дублировать письма или продления. Для страховки добавляют фоновую сверку статусов подписок по расписанию.

Как тестировать вебхуки локально?

Через Stripe CLI: команда stripe listen пробрасывает события на localhost, а stripe trigger генерирует тестовые события любого типа — от успешной оплаты до провала списания. Это позволяет отладить все ветки обработчика до продакшена.

Автор

Fullstack Engineer

В коммерческой разработке с 2011 года — больше 15 лет. Автор двух SaaS-продуктов: TeleGo.io и LingvoHabit.com. Статьи здесь выросли из реальной клиентской и продуктовой работы.

Подробнее об автореНаписать в Telegram

Похожие статьи

TELEGRAM
TELEGRAM · '26

ИИ-поддержка клиентов: Как собрать RAG-бота с бесшовной передачей менеджеру

Техническое руководство по проектированию бота поддержки в Telegram с использованием OpenAI Embeddings, PostgreSQL Pgvector и гибридной системы эскалации на оператора.

#Telegram #AI #RAG #LLM #Node.js #PostgreSQL
4 мин. чтения
SAAS
SAAS · '26

Как собрать MVP для B2B SaaS за 30 дней без технического долга

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

#SaaS #MVP #Архитектура #Nuxt #Node.js
4 мин. чтения
SAAS
SAAS · '26

Сколько стоит разработка SaaS MVP в 2026 году: смета по этапам

Из чего складывается цена SaaS MVP — от $4 000 за рабочую версию с биллингом. Разбор этапов, сроков, стека и расходов после запуска без маркетинговой воды.

#SaaS #MVP #Цены #Стартапы
3 мин. чтения