Hunchbite
ServicesGuidesCase StudiesAboutContact
Start a project
Hunchbite

Software development studio focused on craft, speed, and outcomes that matter. Production-grade software shipped in under two weeks.

+91 90358 61690hello@hunchbite.com
Services
All ServicesSolutionsIndustriesTechnologyOur ProcessFree Audit
Company
AboutCase StudiesWhat We're BuildingGuidesToolsPartnersGlossaryFAQ
Popular Guides
Cost to Build a Web AppShopify vs CustomCost of Bad Software
Start a Project
Get StartedBook a CallContactVelocity Program
Social
GitHubLinkedInTwitter

Hunchbite Technologies Private Limited

CIN: U62012KA2024PTC192589

Registered Office: HD-258, Site No. 26, Prestige Cube, WeWork, Laskar Hosur Road, Adugodi, Bangalore South, Karnataka, 560030, India

Incorporated: August 30, 2024

© 2026 Hunchbite Technologies Pvt. Ltd. All rights reserved.· Site updated February 2026

Privacy PolicyTerms of Service
Home/Guides/Stripe Billing for SaaS: Subscriptions, Usage-Based Pricing, and the Edge Cases That Break Things
Guide

Stripe Billing for SaaS: Subscriptions, Usage-Based Pricing, and the Edge Cases That Break Things

A practical guide to implementing Stripe billing in a SaaS product — covering subscription models, metered usage, free trials, dunning, proration, and the specific edge cases that break billing implementations and cause revenue leakage.

By HunchbiteMarch 30, 202611 min read
StripebillingSaaS

Billing is the one part of your product that directly touches revenue. Get it wrong and you lose money silently — failed payments that never retry, plan changes that prorate incorrectly, trials that don't convert because the confirmation email wasn't sent. Unlike a broken feature that users complain about, broken billing often fails without visible symptoms until you audit your numbers months later.

This guide covers the implementation patterns that work and the specific edge cases that break billing in production. Not a tour of the Stripe documentation — Stripe's docs are excellent. This is what you need to know that the docs don't emphasize.

The 3 billing models and their Stripe implementation

1. Flat-rate subscription

One price, recurring monthly or annually. The simplest model and still the most common in B2B SaaS.

In Stripe: a Product represents what you're selling, a Price represents the billing interval and amount, and a Subscription ties a Customer to a Price. The implementation is straightforward: create the subscription when the customer completes signup, handle the customer.subscription.created webhook to provision access, and listen for invoice.payment_failed to handle payment failures.

The edge cases still exist even here. Trials, plan changes, and failed payments all need explicit handling — which we'll cover below.

2. Per-seat pricing

A recurring subscription where the quantity scales with the number of users. The price is per seat per month; the subscription quantity is the current seat count.

In Stripe: use Subscription with a quantity parameter. When a customer adds a user, call stripe.subscriptions.update to increment the quantity. When they remove a user, decrement it. Stripe prorates the difference automatically (if you configure it to).

The subtlety: decide whether seat additions bill immediately (prorated charge for the remainder of the current billing period) or at the start of the next period. Most SaaS products charge immediately for additions (you're unlocking access now) and bill removals at the end of the period (you've already paid for this month). This needs to be explicit in both your Stripe configuration and your customer-facing terms.

3. Usage-based / metered billing

The customer pays for what they consume — API calls, active users, compute time, storage. The price is unknown until the end of the billing period.

In Stripe: use Stripe Meters. Define the meter (what unit you're counting), report usage events via stripe.billing.meters.createEvent() as they happen in your application, and attach the meter to a price in your subscription. Stripe aggregates usage per billing period and generates the invoice automatically.

This is the most complex model to implement correctly. The reporting pipeline needs to be reliable — usage events that fail to reach Stripe mean you don't bill for that usage. A queue-based approach (write usage to your database, a background job sends it to Stripe, with retries on failure) is more reliable than reporting usage inline in the request path.

The webhook architecture

Webhooks are the foundation of correct billing. Not an integration option — the core mechanism.

Stripe webhooks are HTTP POST requests that Stripe sends to your application when billing events happen. Payment succeeds, subscription cancels, invoice generates, trial ends — these all arrive as webhooks. If your application doesn't handle them, billing state in your database falls out of sync with billing state in Stripe.

The architecture that works in production:

  1. One webhook endpoint. All Stripe events go to /api/webhooks/stripe. Verify the signature on every request. Return 200 immediately.
  2. Queue, don't process inline. Enqueue the event payload, process it asynchronously. This decouples webhook receipt from processing, handles slow database operations, and survives application restarts mid-processing.
  3. Idempotent handlers. Stripe retries webhooks that don't receive a 200 response (or receive a 5xx). Your handlers will receive the same event multiple times. Use the Stripe event ID as an idempotency key — check whether you've already processed this event before doing work.
  4. Dead-letter logging. Events that fail processing after N retries go to a dead-letter queue or alert channel. Don't silently drop them.

The events you must handle without exception: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed, and customer.subscription.trial_will_end.

The 8 edge cases that break billing implementations

1. Failed payment handling

A payment fails. What happens next?

Stripe has built-in retry logic (Smart Retries — it uses ML to pick retry times with higher success rates). Configure this in your Stripe dashboard. But retries alone aren't enough. You need a dunning flow: a sequence of emails over the retry window telling the customer their payment failed and asking them to update their card.

The hard part is access revocation. Do you immediately revoke access when payment fails, or after the retry window expires? Most SaaS products give a grace period (7–14 days) before revoking access, with escalating communication. This needs to be explicit in your code — the invoice.payment_failed webhook starts the grace period clock; the customer.subscription.deleted event (after Stripe finally gives up) ends it.

Without handling this explicitly, failed payments either silently accumulate (customer never hears from you, keeps using the product for free) or immediately revoke access on a transient payment failure (aggressive, causes churn).

2. Plan upgrades mid-cycle

A customer on your $49/month plan upgrades to the $99/month plan on day 15 of their billing cycle.

Stripe can handle proration automatically. But you need to decide: charge the prorated difference immediately, or apply it to the next invoice? The Stripe default is immediate proration with an immediate charge. Some SaaS products prefer to credit the unused portion of the current plan and apply it to the next invoice instead.

Set proration_behavior explicitly when updating subscriptions. The options are create_prorations (immediate charge), none (no proration, full new price next cycle), or always_invoice (creates a proration invoice immediately).

3. Downgrades

A customer downgrades from $99 to $49. Unlike upgrades, immediate access reduction is usually wrong — they paid for the full month at the higher tier.

Set proration_behavior: 'none' on downgrades and schedule the price change to take effect at the end of the current billing period using billing_cycle_anchor or subscription schedule items. The customer keeps their current plan until renewal, then drops to the lower tier.

If you implement this incorrectly, you either credit the customer nothing on the downgrade (charging them for features they no longer have access to) or issue an immediate credit and immediately revoke access (which feels punishing).

4. Trial-to-paid conversion

Free trials are standard SaaS practice. The webhook you must not miss: customer.subscription.trial_will_end — Stripe sends this 3 days before the trial ends. Use it to send a conversion email.

When the trial ends, Stripe automatically attempts to charge the payment method on file. If none exists, the subscription goes into incomplete status. You need to handle this: prompt for payment before trial end, and handle the state where trial expired without a payment method.

The invoice.payment_succeeded event after trial expiry is your signal to move the customer from trial to active status in your database. Do not rely on the subscription status field alone for this — check the invoice.

5. Annual to monthly switches

A customer on annual billing wants to switch to monthly. This is operationally messy.

They've paid for the full year. Do you refund the unused months and switch immediately? Or run out the year and switch at renewal? Most SaaS products do the latter (simpler, less accounting). Be explicit in your UI about which they're getting.

If you do immediate switches, you're looking at partial refunds via stripe.refunds.create and a new subscription. The Stripe subscription objects don't cleanly handle mid-year plan interval changes — you often end up canceling and recreating the subscription. Test this flow thoroughly.

6. Seat addition and removal

Per-seat products have this constantly. A team admin adds 3 users and removes 2 in the same week.

Use Stripe's quantity update. But track the subscription quantity in your database independently so you can reconcile. If a webhook fails to deliver and your database says 10 seats but Stripe says 8, the next time you try to update quantity you'll send an incorrect value.

Keep your source of truth in Stripe for billing, your database for access control, and a reconciliation job that runs daily to detect divergence.

7. Refunds and credits

Stripe refunds are straightforward via stripe.refunds.create. The complication is how you represent them in your product and your accounting.

For partial refunds (discounts, goodwill credits), prefer Stripe Customer Balance or Stripe Coupons over manual refunds. They apply cleanly to future invoices and don't create complex reconciliation problems.

For full refunds on an annual plan where the customer wants out early, decide your policy before you need it. "No refunds after 14 days" or "prorated refunds" — either is fine, but it needs to be in the code before a customer asks.

8. Invoice item reconciliation

Every Stripe invoice has line items. Those line items need to be reflected in your database — not just "customer paid $99" but what they paid for, when, and what period it covers.

This matters for: support ("did this charge cover their January billing period?"), accounting (revenue recognition), and analytics (MRR calculations). Store the Stripe invoice ID, subscription ID, period start, period end, and line items. The invoice.payment_succeeded webhook gives you everything you need.

Stripe vs. Paddle vs. Lago vs. Orb

Tool Use when Avoid when
Stripe Billing Your primary billing for almost all SaaS You need complex usage models that Stripe Meters can't represent
Paddle You need a merchant of record (Paddle handles tax, compliance, payouts for 200+ countries) You're US-only and comfortable handling your own tax logic
Lago Open-source billing, complex usage pricing is your core model, you want to self-host billing infrastructure You don't want to maintain billing infrastructure
Orb Usage-based is your primary model and you need flexible aggregation logic (sum, max, unique count, custom) Flat-rate subscriptions — overkill and more expensive

For most SaaS products: Stripe Billing. Paddle if you're selling globally and want tax handled for you. Lago or Orb only if usage-based pricing is genuinely complex (multiple meters, tiers, custom aggregation) and Stripe Meters can't model it.

Testing billing properly

The most common mistake: testing billing manually through the Stripe dashboard. This doesn't scale and doesn't catch the edge cases.

Stripe test mode gives you test card numbers, test webhooks, and test customers. All your CI tests should run in test mode with a test Stripe account. Use the Stripe CLI (stripe listen --forward-to localhost:3000/api/webhooks/stripe) to forward webhooks to your local development environment.

Test Clock is the feature most billing implementations don't use but should. A Test Clock is a simulated time object you attach to a test customer. You can advance it forward — "simulate that 30 days have passed" — and watch subscription renewals, trial expirations, and invoice generation happen in your test environment. This is the only reliable way to test time-dependent billing flows without waiting for them to occur naturally.

Cover these scenarios in your test suite with Test Clocks:

  • Successful subscription creation and first payment
  • Failed payment and retry
  • Trial to paid conversion (with and without payment method on file)
  • Plan upgrade mid-cycle
  • Plan downgrade (effective next period)
  • Subscription cancellation and access revocation

For the full picture of what goes into a B2B SaaS MVP beyond billing — including architecture, multi-tenancy, and what to build in which order — see building a B2B SaaS MVP. For multi-tenant architecture specifically, multi-tenant SaaS architecture covers the row-level security vs. schema-per-tenant decision.

Building a SaaS and need billing done right the first time?

Billing is the part most development teams underestimate. Hunchbite builds SaaS products with Stripe Billing implemented correctly — subscription management, usage tracking, dunning flows, and webhook architecture that doesn't leak revenue.

→ Software Development Agency

Call +91 90358 61690 · Book a free call · Contact form

FAQ
Should I use Stripe Billing or just Stripe Payments with my own subscription logic?
Use Stripe Billing. Building your own subscription logic on top of Stripe Payments sounds simpler until you're three months in and implementing proration, dunning retry schedules, invoice line item reconciliation, and trial-to-paid conversion logic. Stripe Billing handles all of this and has been hardened against the edge cases that will break your custom implementation. The only reason to avoid Stripe Billing is if your pricing model is so custom that Stripe's objects can't represent it — which is rare. For 95% of SaaS pricing models, Stripe Billing is the correct choice.
How do I handle Stripe webhooks reliably in Next.js?
Three requirements: verify the webhook signature using Stripe's SDK (stripe.webhooks.constructEvent with the raw request body and your webhook secret), make every webhook handler idempotent (Stripe can deliver the same event multiple times), and process webhooks asynchronously — acknowledge receipt immediately with a 200 response, then process the event in a background job. In Next.js App Router, create a route handler at app/api/webhooks/stripe/route.ts, use the raw body (not the parsed JSON), verify the signature, enqueue the event, and return 200. Never do synchronous database writes in the webhook handler if they might fail or time out.
What's the best way to implement usage-based pricing with Stripe?
Use Stripe Meters (the current recommended approach, which replaced legacy metered billing). Define a meter for your billable unit (API calls, active seats, GB stored), report usage events via the Stripe Meters API as they occur in your application, and attach the meter to a price on your subscription. Stripe aggregates usage and automatically bills at the end of the billing period. The key implementation detail: report usage events reliably — use a queue or background job for usage reporting, don't do it inline in the request path, and handle failures with retries. Lost usage events mean lost revenue.
Next step

Ready to move forward?

If this guide resonated with your situation, let's talk. We offer a free 30-minute discovery call — no pitch, just honest advice on your specific project.

Book a Free CallSend a Message
Continue Reading
guide

Drizzle ORM Setup Guide: Type-Safe Database Access with PostgreSQL

How to set up Drizzle ORM with PostgreSQL from scratch — schema definition, migrations, query patterns, connection pooling, and the configuration decisions that matter in production Next.js applications.

11 min read
guide

How Database Indexes Work (And Why the Wrong Index Is Worse Than None)

A technical guide to database indexes: B-tree internals, composite index column ordering, covering indexes, partial indexes, the write cost of over-indexing, EXPLAIN ANALYZE interpretation, and the common indexing mistakes that degrade production performance.

14 min read
All Guides