Two billing models, six mechanical differences in Stripe. A decision framework for solo and indie SaaS founders picking which one to ship.
Pick one-time when the value you deliver is a discrete, one-shot outcome (a template, a calculator, a course, a lifetime tool). Pick subscription when the value is ongoing delivery (hosting, monitoring, content). Supporting both roughly doubles the Stripe surface area, so most indie SaaS founders should commit to one and ship deeper, not wider.
A billing model decision is the architectural choice between charging customers once for an outcome (one-time billing, Stripe Checkout in mode: 'payment') or charging them on a recurring cycle for ongoing access (subscription billing, Stripe Checkout in mode: 'subscription'). The decision determines which Stripe webhook events your app listens for, how much customer data you retain, whether you need dunning infrastructure, and how refunds map to access revocation.
Stripe Checkout in mode: 'payment'. One charge per customer. One checkout.session.completed event per purchase. Guest customers by default. No recurring contract to keep alive.
Best for: discrete value delivery (templates, courses, tools, lifetime deals)
Stripe Checkout in mode: 'subscription'. Recurring charges per cycle. Five or more lifecycle events per customer. Customer object required. Smart Retries dunning included.
Best for: ongoing value delivery (hosting, monitoring, content, continuous service)
The decision is not about pricing strategy; it is about which Stripe surface area your app commits to building and maintaining. These six categories cover the full mechanical diff.
| Dimension | One-Time | Subscription |
|---|---|---|
| Stripe Webhook Events | ||
| Primary purchase event | `checkout.session.completed` | `checkout.session.completed` + lifecycle events |
| Subscription lifecycle events | `customer.subscription.created/updated/deleted` | |
| Invoice events | `invoice.paid`, `invoice.payment_failed` | |
| Required handlers (minimum) | 1 | 5+ |
| Customer Object | ||
| Customer object required | No (guest customers by default) | Yes (required for recurring) |
| Customer data retention | Optional, your choice | Required for the customer lifetime |
| PII surface area | Email at minimum, name optional | Email, name, address, payment method on file |
| Stripe Tax | ||
| Tax computation | Once at checkout | On every recurring invoice |
| Tax-rate change handling | N/A after sale | Compounds across customer lifetime |
| Tax-ID validation | One-time check | Ongoing per renewal |
| Failed Payments & Dunning | ||
| Built-in retry layer | Smart Retries + custom rules | |
| Failure status surface | Single failed `checkout.session` | `past_due`, `unpaid`, `canceled` states |
| Customer notification flow | You build it | Stripe-managed emails available |
| Recovery responsibility | Customer re-initiates | Stripe + your dunning rules |
| Refunds & Access Revocation | ||
| Refund model | Single refund per purchase | Partial refunds + cancellation logic |
| Access revocation pattern | Optional (often skipped) | Required (gate every request) |
| Pro-rata math | N/A | Required on mid-period cancel |
| Idempotency & Scale | ||
| Webhook events per customer | 1 | 12+ per year minimum |
| Idempotency dedupe pressure | Bounded by purchases | Scales with customer base |
| Idempotency-Key TTL (Stripe default) | 24 hours | 24 hours |
| Code Surface & Buyer Fit | ||
| Webhook handler files (minimum) | 1 | 3-5 |
| Best for | Templates, calculators, courses, lifetime deals | Hosting, monitoring, content, ongoing service |
| Cash flow shape | Front-loaded | Smoothed across the customer lifetime |
| Churn surface | None (no recurring contract) | Recurring; needs retention metrics |
Webhook event names and Smart Retries / dunning behavior verified against Stripe's official documentation (Checkout, Subscriptions, Idempotency).
Both billing models have a genuine fit. The question is which one fits the value you deliver.
A single `checkout.session.completed` handler is enough to fulfill orders. Per Stripe docs: "Process the checkout.session.completed event to fulfill orders when a customer completes their purchase." No lifecycle state machine, no dunning rules, no pro-rata math.
Guest customers are the default. You can ship a one-time SaaS with email-and-payment as the only customer data on file. Less PII to retain means less to encrypt, audit, and rotate when keys leak.
There is no recurring contract to keep alive. Customer service load is bounded by the support window you commit to, not by month-over-month retention. Indie SaaS founders can ship and move on instead of running renewal funnels.
Each purchase fires one event with one Stripe Event ID. Idempotency dedupe is straightforward: a unique constraint on `processed_events.event_id` catches duplicates. Subscription apps generate 12+ events per customer per year just from renewals.
Subscription billing smooths cash flow across the customer lifetime. For SaaS that funds ongoing infrastructure (hosting, monitoring, content delivery), recurring revenue matches the recurring cost structure.
Stripe Smart Retries automatically re-attempt failed payments using machine-learned timing. Per Stripe docs: subscriptions can transition to `past_due` and Stripe re-attempts payment "using Smart Retries or based on your custom retry rules." Recovered payments add directly to ARR.
The full event surface (`subscription.created`, `subscription.updated`, `subscription.deleted`, `invoice.paid`, `invoice.payment_failed`) lets your app track entitlement state continuously. Tier upgrades, downgrades, and pauses all map to specific events.
For B2B SaaS billing across jurisdictions, Stripe Tax computes the right rate on every invoice. The cost of managing this manually for a SaaS with even a few hundred customers across regions usually outweighs the operational overhead of running subscriptions.
Decision rules grounded in the value you deliver, not in pricing psychology.
Both models are valid. The decision is not philosophical; it is architectural. Subscription billing adds at least four lifecycle event handlers, a dunning state machine, ongoing customer data retention, pro-rata refund math, and Stripe Tax computation on every invoice. That overhead is justified when the value you deliver is itself recurring. It is not justified when you are selling a one-shot outcome dressed up as recurring access.
For developer-tool SaaS in particular, one-time pricing aligns price with value delivery. A template is adopted once. A calculator is used until the customer outgrows it. A course is completed. Wrapping these in a subscription forces the founder to manufacture ongoing reasons to charge, which usually shows up as feature bloat, artificial limits, or aggressive dunning emails. None of those are good for the brand.
SecureStartKit defaults to mode: 'payment' in its checkout flow. The template ships dual-mode webhook infrastructure and a schema with both `purchases` and `subscriptions` tables, so switching to subscription is a one-line change in `actions/billing.ts`. We picked one-time as the default because a template is a one-time architectural decision, but we kept the subscription handlers in the codebase because the architectural cost was already paid. If your SaaS delivers ongoing value, flip the switch and ship subscription; the infrastructure is already there.
If you already shipped subscription and want to switch, the Stripe subscription to one-time migration playbook covers the cancellation modes, the dual-mode webhook handler, and the five access-revocation traps that ambush founders mid-migration.
Discrete value. One handler. SecureStartKit's choice.
Ongoing value. Full lifecycle. Worth the overhead when fit.
All five run on the security-first one-time pricing model.
SecureStartKit defaults to mode: 'payment' in the checkout flow. The webhook handler and schema support both modes; switching is one line. The pricing page below is the live worked example.