· 6 min čtení

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.

PaymentsSaaSStripe

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

ProjektModelPricingAudienceConversion
MaruškaSubscription, 3 tiers399 / 599 / 799 Kč/měsMikrofirmy, regulární output2.1 %
Holky s úspěchemOne-shot Checkout990 Kč jednorázověSporadická, course-buyer4.7 %
DokladBotSubscription199 Kč/měsÚčetní firmy3.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 fail

Dunning 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:

EventKdyAkce
checkout.session.completedPo dokončení Checkout (one-shot i subs)Grant access, send email
invoice.paidRecurring charge prošelExtend access, send invoice email
invoice.payment_failedRecurring charge failnulNotify user, schedule retry (Stripe sám)
customer.subscription.deletedCancellationRevoke access at period_end
customer.subscription.updatedTier changeUpdate 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

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.