5 Things We Got Wrong About Stripe Billing (So You Don't Have To)
Stripe is excellent. Their documentation is among the best in the industry. Their APIs are well-designed and developer-friendly. And yet, across five product launches, we managed to get billing wrong in five different ways. Not because Stripe is hard to use, but because billing is hard to think about correctly when you are focused on building features.
We are Obsidian Clad Labs, a small group of friends from Tennessee. We run five SaaS products, each with its own pricing, plans, and billing logic. Here are the five mistakes we made so you can learn from our pain instead of your own.
Mistake 1: Not Testing Webhooks Thoroughly Before Going Live
Stripe communicates subscription changes to your backend through webhooks — HTTP POST requests sent to an endpoint on your server whenever something happens: a subscription is created, a payment succeeds, a payment fails, a subscription is canceled. Your backend needs to handle each of these events correctly to keep your user's account state in sync with their billing state.
We wrote the webhook handler. We tested it with a couple of test events. It looked fine. Then we went live, and within the first week, we discovered that our handler silently dropped events it did not recognize, did not properly verify webhook signatures (meaning anyone could have spoofed a payment confirmation), and crashed on edge cases like a subscription being updated and canceled in the same second.
The fix was to rebuild the webhook handler with proper signature verification, exhaustive event handling (including a catch-all that logs unrecognized events rather than dropping them), and retry logic for database writes that might fail. We also started using Stripe's webhook testing tools to simulate every event type before deploying changes.
The lesson: Your webhook handler is the most critical piece of your billing integration. Test it with every event type Stripe can send, not just the happy path. Use Stripe's CLI to forward test events to your local development server. Verify signatures. Log everything. A webhook bug in production means users paying for features they cannot access, or worse, accessing features they are not paying for.
Mistake 2: Not Handling Failed Payment Emails
When a subscription payment fails (expired card, insufficient funds, bank decline), Stripe retries the charge on a schedule. But by default, Stripe does not send the customer a particularly prominent notification about the failure. The customer might not even know their payment failed until their subscription is canceled weeks later after all retry attempts are exhausted.
We did not build any dunning communication into our products. No "your payment failed, please update your card" email. No in-app banner. No nothing. The result was that we had users whose subscriptions quietly entered a "past due" state, and neither they nor we knew about it until the subscription was automatically canceled and the user lost access to paid features. That is a terrible user experience and a completely avoidable revenue loss.
The lesson: Build dunning emails from day one. When you receive an invoice.payment_failed webhook, immediately email the customer with a clear explanation and a direct link to update their payment method. Send a reminder 3 days later if the issue is not resolved. And display an in-app banner on every page until the payment is recovered. Stripe's customer portal makes it easy for users to update their card — you just need to tell them to do it.
Mistake 3: Creating Too Many Products and Prices
Every time we changed our pricing, added a promotional offer, or created a temporary discount, we created new Product and Price objects in Stripe. After a few months across five products, our Stripe dashboard was cluttered with dozens of products and hundreds of prices, many of which were no longer active but could not be deleted because they were attached to historical subscriptions.
Stripe does not let you delete Products or Prices that have ever been used in a transaction. You can archive them, but they still show up in API responses unless you explicitly filter them out. Our code had to handle the distinction between active and archived prices, our dashboard was a mess, and onboarding a new team member to understand our billing structure required a 30-minute walkthrough of which products were real and which were legacy.
The lesson: Plan your product and price structure carefully before going live. Use Stripe's test mode to experiment with different structures. Use coupons for discounts instead of creating new prices. If you need to change pricing, create a new price on the existing product rather than a new product entirely. Keep the number of active prices per product to the minimum needed. Future you will thank present you for a clean Stripe dashboard.
Mistake 4: Not Implementing the Full Subscription Lifecycle
A subscription has more states than just "active" and "canceled." The full lifecycle includes: trialing (free trial period), active (paying and current), past_due (payment failed, retrying), unpaid (all retry attempts exhausted), canceled (subscription ended), and incomplete (initial payment did not succeed). Each of these states requires different behavior in your application.
We initially only handled "active" and "canceled" — if you were active, you got paid features; if you were canceled, you got free features. This broke in multiple ways. Users in a "past_due" state still had access to paid features even though they were not paying. Users in "trialing" state were treated as free users because our code did not recognize the trialing status. Users whose initial payment failed were stuck in "incomplete" limbo with no clear path forward.
The lesson: Handle every subscription status from day one. Map each status to a specific application behavior: trialing gets full access, active gets full access, past_due gets full access with a warning banner and dunning emails, unpaid gets downgraded to free, canceled gets downgraded to free, incomplete gets a "complete your payment" prompt. Write this mapping down in a document and make sure your webhook handler and your access control logic both agree on it.
Mistake 5: Not Setting Up Tax Collection Early
In many states and countries, SaaS products are subject to sales tax. We did not think about this until we had been collecting payments for weeks. Retroactively figuring out which transactions should have had tax collected, in which jurisdictions, at what rate, was a nightmare we could have avoided entirely by enabling tax collection from the start.
Stripe has a built-in tax product that automatically calculates and collects the correct tax rate based on the customer's location. It requires some setup — you need to configure your business address, enable tax collection on your products, and choose whether prices are tax-inclusive or tax-exclusive — but once configured, it handles the complexity automatically. The dashboard gives you tax reports you can use for filing.
The lesson: Enable Stripe Tax before your first transaction. Not after your first 10 transactions. Not when you "get around to it." Before transaction number one. The setup takes an hour. The cleanup if you skip it takes much longer and involves uncomfortable conversations with an accountant about retroactive tax obligations. If Stripe Tax is not available in your jurisdiction, use a third-party tax calculation service. Either way, do not ignore sales tax just because you are small.
The Billing Checklist We Wish We Had
Based on everything we got wrong, here is the checklist we now use before launching billing on any new product:
Webhook handler verifies signatures and handles all event types. Failed payment triggers dunning email to customer within one hour. Product and price structure is clean (one product per plan, minimal prices). All six subscription statuses are mapped to application behavior. Tax collection is enabled and configured before the first transaction. Customer portal is configured so users can manage their own billing. Webhook endpoint is monitored and alerts on failures. Test mode has been used to simulate the full subscription lifecycle end-to-end.
None of these items are difficult individually. But if you skip any of them, you will discover the gap at the worst possible time — when a real customer with a real credit card hits an edge case that your billing code does not handle. Fix it before that happens.
Billing Is a Feature, Not an Afterthought
The biggest mindset shift we had to make was treating billing as a first-class feature of our products, not as a backend detail that "just works." Billing touches every part of the user experience: signup, onboarding, feature access, plan upgrades, failed payments, cancellations, and reactivations. If any of those touchpoints are broken, the user experience is broken, regardless of how good your core product is.
Across five products and hundreds of billing-related decisions, we have learned that the time invested in getting billing right pays for itself many times over. Every hour spent on proper webhook handling, dunning emails, and lifecycle management is an hour you do not spend debugging payment issues, apologizing to confused customers, or manually fixing account states in your database.
All those billing lessons went into TeachShield.
We built TeachShield with the billing infrastructure we wish we had from day one. Simple plans, clean upgrades, and a free tier that lets you try everything before you pay.