Billing that needs a person every week
A customer upgrades mid-month and gets charged the full price twice. Another's card fails and they keep using the product for months without paying because nobody noticed. Someone downgraded but still has access to the premium features. The finance person keeps a spreadsheet of "billing exceptions" and asks a developer to change things in the database directly.
Every new plan or price change becomes a small project, because the billing logic is spread across the codebase and nobody is quite sure what depends on what.
How billing gets into this state
In the early days, billing was built quickly to start taking money, which was the right call. Usually that meant a payment form, a flag on the account saying "paid", and a few conditions in the code checking the plan name.
Then the product grew. Annual plans were added. Then seats. Then a discount for a partner. Then a legacy price for early customers. Each was patched onto the original design. The result is two sources of truth, the payment provider and the app's own records, which slowly disagree.
- Plan limits hard-coded by plan name throughout the code.
- Webhooks from the payment provider not handled, or handled without retries.
- No proration logic, so mid-cycle changes are calculated by hand.
- Failed payments not followed up, or followed up by email from a person.
- No record of why a customer's access differs from their plan.
What messy billing costs
| Problem | Cost |
|---|---|
| Unpaid access continuing | Revenue you are owed but not collecting |
| Double or wrong charges | Refunds, complaints and chargebacks |
| Manual fixes in the database | Developer time and a real risk of mistakes |
| Pricing changes are slow | You avoid testing new prices because it is painful |
| No reliable revenue figures | Board and investor numbers built on guesswork |
How we rebuild SaaS billing
- Map what exists. Every plan, price, discount, legacy arrangement and exception, including the ones in the spreadsheet, so nothing is lost in the move.
- Make Stripe the source of truth. Products, prices and subscriptions live in Stripe Billing. The app reads subscription state from it rather than keeping its own version.
- Handle webhooks properly. Every subscription and invoice event is received, verified, stored and processed idempotently, with retries, so a missed event cannot leave an account in the wrong state.
- Separate entitlements from plan names. The app checks what a customer is entitled to, such as seats or features, rather than which plan they are on, so new plans do not mean code changes everywhere.
- Handle changes and failures. Upgrades and downgrades with proration, trials that convert, failed payment retries with emails, and a clear rule for when access is limited.
- Give your team a billing screen. Support and finance can see a customer's subscription, invoices and entitlements, apply credits and make changes with a record of who did what.
- Migrate carefully. Existing customers move to the new setup with their current prices kept, checked account by account before switch-over.
If you need usage-based pricing, tax handling across countries or invoices for larger customers, those are built into the same model rather than bolted on.
Billing that runs itself
The finance spreadsheet of exceptions goes away, because exceptions are now recorded where they belong: as prices, coupons or credits in the billing system, each with a note of who applied it and why.
Customers are charged correctly when they change plans. Failed payments are followed up automatically, and access changes when the rules say it should. Support can answer billing questions from a screen. New plans and price tests become a configuration change rather than a development project, and the revenue figures you report are ones you can trust.
Recognise any of this?
- Someone keeps a spreadsheet of billing exceptions.
- Developers change subscription data in the database by hand.
- Some customers have access they are not paying for.
- Adding a new plan means code changes in many places.
- Your app and your payment provider disagree about who is subscribed.