SaaS Billing Done Right: Plans, Trials, Proration and Dunning
How to build subscription billing that holds up: a billing provider for the money, webhooks as the source of truth, entitlements in your app, and clear rules for upgrades, trials and failed payments.

Charging a card once a month is the easy tenth of SaaS billing. The rest arrives later: a customer upgrades in the middle of a cycle, a renewal fails on a Sunday, a trial ends with no card on file. Each case needs a rule, and the rules have to agree with what your application lets the customer do.
The approach that holds up is to let a billing provider own the money side and keep your own code to a thin layer that records what each account is entitled to.
Use a billing provider and keep your own code small
A subscription engine has to schedule renewals, generate invoices, retry failed charges, handle card authentication and keep an audit trail an accountant will accept. None of it makes your product better. Providers such as Stripe Billing, Paddle, Chargebee and Recurly exist for this job.
With a provider, card details go from the customer's browser straight to its hosted fields or checkout page, so card numbers never reach your servers. Your database only keeps a copy of who has paid.
One early choice is hard to reverse. With a payment processor you are the seller, and tax registration and remittance are yours to manage. With a merchant of record, the provider resells your product and is the seller on the invoice. Paddle, for example, describes itself as the seller on record and takes on collecting and paying sales tax and VAT. Choose before writing code, because live subscriptions do not move between providers easily.
Separate what a customer pays for from what they can do
The most common billing bug lives outside the billing code: a line like if ($account->plan === 'pro') repeated in forty places. The first time you add a plan or sell an add-on, every one of those lines is wrong. Keep three layers apart:
- Catalog. Products and prices live in the provider. A small table maps each provider price ID to an internal plan key, so price IDs never appear in controllers or templates.
- Subscription state. One row per account: the provider's customer and subscription IDs, the status, the plan key, the end of the current period, any scheduled cancellation and the trial end date.
- Entitlements. A single place that turns a plan into capabilities and limits: seats, projects, API calls, access to exports. Code asks "can this account export?" and never asks which plan it is.
A custom deal for one large customer is then a row of overrides and not a new branch in the code. Stripe has a built-in version of the idea: you attach features to products, and its Entitlements API reports which features each customer has. Keep a local copy even then, so an access check never waits on an outside API.
Treat webhooks as the source of truth
The page a customer lands on after checkout proves nothing. They may have closed the tab before the redirect, and renewals, failures and portal cancellations happen with no browser session at all. The provider reports all of it through webhooks, and that is the only channel your subscription state should be written from.
A handler that survives production does these things in order:
- Verify the signature against the raw request body. Stripe signs each event in the
Stripe-Signatureheader with the endpoint's secret. An unverified endpoint lets anyone grant themselves a paid plan. - Record the event ID and skip events already processed. The same event can be delivered more than once.
- Return a 2xx status quickly and do the real work in a queued job.
- Do not rely on order. Events can arrive out of sequence, so fetch the current subscription from the API and store what it says now.
- Alert on failures. Stripe's webhook documentation describes automatic retries over a limited window. After that, the event is not sent again on its own.
- Reconcile nightly. A scheduled job compares local subscription rows with the provider and reports differences.
On Laravel, Laravel Cashier ships a webhook controller with signature verification and keeps its subscription tables in sync.
Upgrades, downgrades and proration
Proration is the arithmetic for a change in the middle of a billing period: a credit for the unused part of the old price and a charge for the remaining part of the new one. The provider does the math. You set the policy.
Upgrades take effect immediately. Decide whether the difference is charged at once or added to the next invoice. In Stripe that is the proration_behavior parameter on a subscription update: create_prorations adds proration items to the upcoming invoice, always_invoice bills them right away, and none skips proration. The proration guide covers the edge cases.
Downgrades take effect at the end of the period. The customer keeps what they paid for and there is no refund to explain.
Show the amount before the customer confirms. Providers can preview the invoice for a proposed change, and a surprise charge is a dispute waiting to happen.
One rule is yours alone: what happens when an account downgrades below its current usage. Twelve projects on a plan that allows five should not lead to deleted data. Make the extra items read-only, or block the downgrade until the customer archives some.
Trials that end without surprises
Asking for a card at the start of a trial means fewer signups and a clean conversion. No card means more people try the product, and each of them has to come back and pay.
For trials without a card, decide what happens on the last day. Stripe exposes this as a setting on the subscription, with options to cancel it, pause it or create an invoice when no payment method is present. Whichever you pick, your application needs a matching state: a locked account with its data intact and a clear path to add a card.
Send the reminder from the provider's trial-ending event and not from your own timer, so the two can never disagree. Card networks and some jurisdictions also set rules for trial reminders and cancellation. Confirm them in your provider's documentation and with your counsel.
Failed payments, dunning and when access stops
Cards expire and get replaced, so some renewals fail for customers who fully intend to pay. Dunning is the process of recovering those payments through scheduled retries and reminders.
Use the provider's retry logic. Stripe calls its version Smart Retries and lets you choose what happens when the retries run out: cancel the subscription, mark it unpaid or leave it past due. Your part is the product side of the same timeline:
- While a subscription is past due, keep access on and show account owners a banner that links to the card form.
- Let customers fix it themselves. A hosted customer portal handles cards, invoices and cancellation.
- When retries are exhausted, restrict the account and keep its data for a stated period, so a late payment restores everything.
Retry timing and messaging get a fuller treatment in reducing subscription churn from failed payments.
Taxes are a job for a specialist
What you owe in sales tax, VAT or GST on a subscription depends on where your company is, where each customer is, what you sell and how much you sell there. Nothing here is tax advice, and a developer should not be the person deciding where you register.
What the software has to do is narrower. Collect a billing country or address at checkout. Offer business customers a field for their tax ID. Pass both to the provider and let its tax product, or the merchant of record, calculate the amount and print it on the invoice. Then have an accountant who works with software companies review the setup before you sell across borders.
What to test in SaaS billing, and when to bring in help
Billing bugs are found by customers unless you rehearse the calendar. Stripe's test clocks move a test subscription forward in time. Before launch, run each of these and compare the provider's records with your own tables: a trial converting to paid, a trial expiring unpaid, an upgrade mid-cycle, a downgrade at period end, a failed renewal that later succeeds, and the same webhook delivered twice.
A small team with one product, flat plans and a framework integration can ship this in-house. It becomes specialist work when billing is per seat across many tenants, when usage is metered, or when live subscriptions have to move from another system. We build this for startups and SaaS companies, usually within Laravel SaaS development, where billing sits beside the tenancy model among the first decisions.
Frequently asked questions
Should I choose a payment processor or a merchant of record?
A processor gives you more control and makes you the seller, with tax obligations to manage. A merchant of record is the seller on the invoice and handles sales tax and VAT, with its own fees and less control over checkout. Compare the two against the countries you sell to, and ask your accountant before deciding.
How should switching between monthly and annual plans work?
Treat them as two prices of the same product. With most providers, a move from monthly to annual starts a new billing period and charges immediately, with credit for unused time. A move from annual to monthly is usually scheduled for the end of the paid year.
Where should usage data for metered billing live?
In your own system first. Record every billable event with an ID, a timestamp and the account, then report it to the provider. The raw record lets you answer a customer who questions an invoice.
Add worldwincoder.com as a preferred source on Google, or open this article in your AI assistant to use it as a source.
