Stripe Checkout vs Subscriptions: jaký mode kdy použít
Reálný srovnání ze tří projektů - Maruška (subs 399/599/799 Kč), Holky s úspěchem (one-shot 990 Kč), DokladBot (sub 199 Kč/mo). Conversion 4.7 % vs 2.1 %, dunning, customer portal, webhook events.
Nejčastější chybu vidím u early-stage SaaS zakladatelů: automaticky zvolí subscription model, protože "tak to dělají všichni". Pak řeší dunning, vendor lock-in u Stripe Billing a 2 % conversion při 4 % marketing nákladu. Ze tří projektů, kde jsem řešil Stripe (Maruška, Holky s úspěchem, DokladBot), jsem si vyšetřil pravidlo: subscription jen když je opakovaná hodnota zjevná, jinak one-shot Checkout.
Tady je srovnání s reálnými conversion čísly.
Tři projekty, tři billing strategie
| Projekt | Model | Pricing | Audience | Conversion |
|---|---|---|---|---|
| Maruška | Subscription, 3 tiers | 399 / 599 / 799 Kč/měs | Mikrofirmy, regulární output | 2.1 % |
| Holky s úspěchem | One-shot Checkout | 990 Kč jednorázově | Sporadická, course-buyer | 4.7 % |
| DokladBot | Subscription | 199 Kč/měs | Účetní firmy | 3.4 % |
Maruška a DokladBot dávají smysl jako subs - uživatel každý měsíc generuje hodnotu (skenuje účtenky, pošle reminder). Holky s úspěchem prodávají digitální workbook - jednou si stáhneš a máš. Nutit subscription tam by srazilo conversion na polovinu, protože "registruju se jen jednou".
One-shot Checkout: kdy vyhrává
Holky s úspěchem prodává coaching workbook za 990 Kč. Cílovka: ženy 30-50, sporadicky kupují kurzy, chtějí transakci, ne závazek.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function createCheckoutSession(email: string) {
return stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card'],
line_items: [
{
price: 'price_workbook_990',
quantity: 1,
},
],
customer_email: email,
success_url: 'https://holkysuspechem.cz/dekujeme?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://holkysuspechem.cz/workbook',
locale: 'cs',
});
}mode: 'payment' = jednorázová platba. Žádné subscription, žádný customer portal, žádný dunning. 5 řádků kódu, 4.7 % conversion.
Webhook handling je taky jednoduchý:
async function handleWebhook(event: Stripe.Event) {
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session;
await grantWorkbookAccess({
email: session.customer_email!,
sessionId: session.id,
});
await sendDownloadEmail(session.customer_email!);
}
}Jeden event, jeden handler. Když platba projde → grant access + email. Když fail → uživatel vidí Stripe error a zkouší znovu.
Subscription: kdy dává smysl
Maruška skenuje účtenky každý měsíc, posílá VAT reminder, vytváří přehledy. Hodnota se akumuluje měsíčně, ne jednorázově. Tady má subs smysl.
export async function createSubscriptionCheckout(email: string, tier: 'basic' | 'pro' | 'business') {
const priceMap = {
basic: 'price_399_basic',
pro: 'price_599_pro',
business: 'price_799_business',
};
return stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: priceMap[tier], quantity: 1 }],
customer_email: email,
success_url: 'https://maruska.app/welcome?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://maruska.app/pricing',
locale: 'cs',
subscription_data: {
trial_period_days: 14,
},
});
}Klíčový rozdíl: mode: 'subscription' + subscription_data.trial_period_days. Stripe automaticky:
- Vytvoří customer
- Vytvoří subscription s trial
- Pošle invoice po skončení trial
- Retry-uje failnuté charges (dunning)
Dunning: hlavní důvod, proč nejít DIY
Dunning = retry logic pro failnuté platby. Karta expiruje, banka odmítne, customer nemá peníze. Bez dunning conversion z trial → paid spadne o 30 %. Stripe Billing dunning má 4 retries během 15 dní s configurable schedule:
// V Stripe dashboardu nebo přes API:
await stripe.subscriptions.update('sub_...', {
payment_settings: {
payment_method_types: ['card', 'sepa_debit'],
save_default_payment_method: 'on_subscription',
},
});
// Smart retry rules: Stripe automaticky retry-uje 3, 5, 7, 14 dní po failDunning si stavět sám je 2 týdny práce. Stripe to dělá zdarma, používej to.
Customer portal: ušetří hodiny
V Marušce uživatelé chtějí: změnit kartu, downgrade tier, cancelnout, stáhnout invoice. Bez portálu je to 4 různé custom UI flow. Stripe má built-in customer portal za 1 řádek:
export async function createPortalSession(customerId: string) {
return stripe.billingPortal.sessions.create({
customer: customerId,
return_url: 'https://maruska.app/settings',
locale: 'cs',
});
}Redirectneš uživatele na URL, který Stripe vrátí. Portal má lokalizaci, branding (logo + barvy), všechny self-service flows. Šetří mi 2 týdny vývoje a support tickets.
Webhook events: čeho si všímat
Stripe pošle desítky událostí. Pro většinu use-cases stačí 4-5:
| Event | Kdy | Akce |
|---|---|---|
checkout.session.completed | Po dokončení Checkout (one-shot i subs) | Grant access, send email |
invoice.paid | Recurring charge prošel | Extend access, send invoice email |
invoice.payment_failed | Recurring charge failnul | Notify user, schedule retry (Stripe sám) |
customer.subscription.deleted | Cancellation | Revoke access at period_end |
customer.subscription.updated | Tier change | Update entitlements |
import type Stripe from 'stripe';
async function handleStripeWebhook(event: Stripe.Event) {
switch (event.type) {
case 'checkout.session.completed':
return onCheckoutCompleted(event.data.object as Stripe.Checkout.Session);
case 'invoice.paid':
return onInvoicePaid(event.data.object as Stripe.Invoice);
case 'invoice.payment_failed':
return onPaymentFailed(event.data.object as Stripe.Invoice);
case 'customer.subscription.deleted':
return onSubscriptionCanceled(event.data.object as Stripe.Subscription);
case 'customer.subscription.updated':
return onSubscriptionUpdated(event.data.object as Stripe.Subscription);
default:
return; // tichý skip pro zbytek
}
}Klíč: vždy idempotent handlers. Stripe občas pošle stejný event 2×. Použij event.id jako idempotency key v DB.
Decision tree
Uživatel platí jednou (workbook, kniha, kurz)?
→ mode: 'payment' (one-shot Checkout)
→ 4-5 % conversion realistická
Uživatel získává hodnotu měsíčně (SaaS, content, software)?
→ mode: 'subscription'
→ Trial 14 dní
→ Customer portal
→ 2-4 % conversion realistická
Hybrid (setup fee + monthly)?
→ mode: 'subscription' + add-on `add_invoice_items`
→ Pozor na complex tax situations
Lessons
- One-shot conversion 2× one subscription conversion v cílovkách, kde audience kupuje sporadicky.
- Trial 14 dní > 7 dní pro B2B (uživatel potřebuje cyklus). Pro consumer B2C může 7 stačit.
- Customer portal (
billingPortal.sessions) ušetří 2 týdny custom UI. Use it. - Dunning Stripe > vlastní retry logic. 30% rozdíl v conversion z trial → paid.
- Webhook idempotency přes
event.id- Stripe občas pošle duplicate. - Locale: 'cs' v Checkout/Portal - uživatelé vidí Stripe UI v češtině, conversion +12% v mojich projektech.
{CHECKOUT_SESSION_ID}v success_url ti dá session ID v URL. Nepoužívej{customer}placeholder - ten je pro existing customer reuse.
Co dál
- Maruška case study → - projekt s 3-tier subs
- Holky s úspěchem case study → - projekt s one-shot
- DokladBot case study → - single-tier subs
- WhatsApp Business API → - jak Maruška páruje WhatsApp identity se Stripe customer
- Multi-tenant Postgres → - billing layer nad multi-tenant DB
Pokud řešíš billing model pro nový SaaS nebo migraci ze subs → one-shot (nebo opačně), napiš mi. Většinou se rozhodne za 30-minutovou call.