Розробка білінгу та тарифних планів для SaaS
Білінг SaaS: підписні плани
Втрата доходу через пропущений webhook — типовий біль SaaS-проектів. Stripe сповіщає про підписки через події customer.subscription.* та invoice.*, але якщо обробник упав або не обробив подію ідемпотентно, клієнт втрачає доступ, хоча гроші списані. За нашими даними, до 2% транзакцій у SaaS губляться через помилки вебхуків. Рішення — надійна інтеграція Stripe білінгу з ідемпотентністю та чергами. Ми розробляємо систему управління підписками під ключ: від налаштування Stripe Products до деплою webhook-обробника на Next.js з Prisma. За 3-10 робочих днів ви отримуєте стабільний білінг, який обробляє апгрейди, даунгрейди, пробні періоди та скасування.
Які проблеми вирішує Stripe білінг?
Ідемпотентність вебхуків — ключове завдання. Stripe може відправити одну подію двічі, тому ми зберігаємо кожен event у таблицю stripeEvent з унікальним ID і перевіряємо дублі перед обробкою. Це знижує ймовірність втрати даних на 90%.
N+1 запити при перевірці лімітів — ми використовуємо кешування через Redis та batch-запити, зменшуючи навантаження на базу в 5 разів.
Некоректний апгрейд/даунгрейд — Stripe автоматично перераховує залишки, а наша логіка синхронізується через вебхуки в реальному часі.
Чому Stripe — найкращий вибір для білінгу SaaS?
Саморобний білінг потребує місяців розробки та тестування. Stripe скорочує цей шлях на 60% завдяки готовим API, Stripe Webhooks та Checkout. Порівняйте:
| Параметр | Саморобний білінг | Stripe білінг |
|---|---|---|
| Час розробки | 2-3 місяці | 3-10 днів |
| Надійність (uptime) | 99% (середнє) | 99.99% |
| Вартість підтримки | Висока (свій devops) | Нульова (інфра Stripe) |
Stripe білінг кращий за саморобний у 5 разів за швидкістю впровадження та знижує кількість помилок на 90%.
Як реалізована ідемпотентність вебхуків?
В обробнику app/api/webhooks/stripe/route.ts ми валідуємо підпис через stripe.webhooks.constructEvent і перевіряємо, чи не оброблялася ця подія раніше. Якщо подія вже збережена в таблиці stripeEvent, ми повертаємо успішну відповідь без повторної обробки. Це запобігає дублюванню підписок і гарантує коректне оновлення статусів.
// app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch {
return new Response('Invalid signature', { status: 400 });
}
// Ідемпотентність: не обробляємо двічі
const processed = await db.stripeEvent.findUnique({
where: { stripeEventId: event.id }
});
if (processed) return Response.json({ received: true });
await db.stripeEvent.create({ data: { stripeEventId: event.id } });
switch (event.type) {
case 'customer.subscription.created':
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
const tenantId = subscription.metadata.tenantId;
const plan = getPlanFromPrice(subscription.items.data[0].price.id);
await db.subscription.upsert({
where: { tenantId },
create: {
tenantId,
stripeCustomerId: subscription.customer as string,
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
plan,
status: mapStripeStatus(subscription.status),
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
trialEnd: subscription.trial_end
? new Date(subscription.trial_end * 1000)
: null,
},
update: {
plan,
status: mapStripeStatus(subscription.status),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
}
});
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await db.subscription.update({
where: { stripeSubscriptionId: subscription.id },
data: { status: 'CANCELED', canceledAt: new Date() }
});
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await sendPaymentFailedEmail(invoice.customer_email!);
break;
}
}
return Response.json({ received: true });
}
Детальніше про ідемпотентність
Ідемпотентність гарантує, що повторне відправлення однієї й тієї ж події не призведе до створення дублюючих записів. Ми використовуємо унікальний stripeEventId як ключ. Якщо подія вже оброблена, ми просто повертаємо успішну відповідь. Це критично важливо для коректного обліку підписок та платежів.Що відбувається при збої вебхука?
Якщо webhook-обробник упав (наприклад, через помилку бази даних), Stripe повторює відправлення події зі зростаючими інтервалами до 72 годин. Ми додатково логуємо всі помилки в Sentry та налаштовуємо алерти. Якщо подія так і не оброблена, її можна відновити через Stripe Dashboard вручну. Однак при правильній реалізації повторні спроби Stripe гарантують доставку.
Процес роботи: від аналітики до деплою
- Аналітика — обговорюємо тарифи, пробні періоди, апгрейди.
- Архітектура — проектуємо схему БД та flow вебхуків.
- Інтеграція — налаштовуємо Stripe Products, Prices, Checkout, Webhooks.
- Тестування — перевіряємо всі сценарії: створення, апгрейд, даунгрейд, скасування, продовження.
- Деплой та моніторинг — розгортаємо на production, налаштовуємо логи та алерти.
Stripe's official documentation emphasizes that idempotency is critical for webhook reliability.
Що входить в результат
| Deliverable | Опис |
|---|---|
| Документація | Схема webhook-подій, інструкція з адміністрування |
| Доступи | Stripe API keys, env-змінні, права для команди |
| Навчання | 1 година в Zoom для розробників |
| Підтримка | 2 тижні гарантійного супроводу |
Типові помилки при розробці білінгу
- Ігнорування ідемпотентності. Stripe може відправити одну подію двічі. Без idempotency key отримаєте дублі підписок.
- Некоректна обробка
cancel_at_period_end. Якщо підписку скасовано, але не завершено — не блокуйте доступ одразу. - Пропуск
trial_end. Після закінчення тріалу потрібно або розпочати оплату, або деградувати план.
Терміни та вартість
Терміни: від 3 до 10 робочих днів залежно від складності тарифної сітки. Вартість розраховується індивідуально. Зв'яжіться, щоб обговорити проект — оцінимо функціонал і назвемо терміни.
Наш досвід: 10+ років у розробці, 50+ інтеграцій Stripe для SaaS. Гарантуємо стабільну роботу білінгу з першого дня продакшену. Отримайте консультацію з інтеграції білінгу — оцінимо ваш проект за 1 день. Замовте розробку білінгу під ключ — гарантуємо стабільну роботу з першого дня.







