How to Avoid Double Charges, Chargebacks, and Angry Customers
Your billing code runs exactly once per customer per event. There's no retry, no rollback. If the webhook handler fails to provision access, customers wait on support. Test before it hurts.
Your billing code runs exactly once per customer per event. There's no retry, no rollback, no "let's deploy a fix and re-run." If the webhook handler fails to provision access, customers wait on support. If the upgrade flow double-charges, you're issuing refunds and apologies.
The usual development instincts—deploy fast and iterate—don't apply when money is involved. Testing billing integrations requires different strategies: isolated test environments, synthetic customer lifecycles, and explicit coverage of edge cases that production will inevitably surface.
The Stakes Are Different
When a bug appears in your product's core features, you fix it and deploy. Users who encountered the bug might be annoyed, but they refresh and continue. Billing bugs carry different consequences.
Double-charging a customer isn't just a bad experience—it's their money incorrectly taken. Even if you refund immediately, the trust damage is real. Their bank might charge overdraft fees. They'll wonder what else might go wrong. The support interaction required to resolve the situation costs you time and goodwill.
Failing to provision access after successful payment is equally damaging. The customer paid for something and didn't receive it. They'll contact support confused or angry. If support takes more than a few minutes to respond, they might dispute the charge, creating a chargeback that costs you money and affects your processor standing.
Under-charging seems like a smaller problem because customers don't complain, but it accumulates. A proration bug that saves customers five dollars per transaction loses you thousands over time. These bugs are harder to detect precisely because no one reports them.
Billing code demands a higher standard of testing than typical application code. You can't rely on production feedback to catch issues—that feedback arrives as angry customers and financial corrections.
Test Mode: Your Parallel Universe
Payment processors provide test modes that simulate real transactions without moving real money. Test mode is your primary tool for billing integration testing. If you're not using it extensively, every production release is another roll of the dice.
Test mode uses separate credentials from production. Your API calls go to the same endpoints but operate on test data rather than real accounts. Test credit cards produce predictable outcomes: certain numbers always succeed, others always fail with specific error codes.
The key benefit of test mode is reproducibility. You can create the same scenario repeatedly without accumulating real charges. When testing a webhook handler, you can trigger the same event type multiple times until you're confident your code handles it correctly.
Test mode also provides tools production doesn't. You can manually trigger events that would be difficult to produce organically. You can adjust timestamps to simulate delayed webhooks. You can create scenarios that take months to occur naturally, like subscription anniversaries or annual renewal processing.
Configure your local development and staging environments to use test mode credentials exclusively. Keep production credentials out of development entirely—an accidental charge against a real account while debugging is easily avoided by never having production credentials accessible during development.
The Customer Lifecycle Test
The most important billing test simulates a complete customer lifecycle from signup through cancellation. This test catches integration issues that unit tests miss because it exercises the entire flow as a real customer would experience it.
Start by creating a new customer with a test card. Walk through your checkout flow as a user would, verifying each step. Confirm that successful payment creates the expected records in your database. Verify that the customer immediately has access to paid features.
Next, trigger a billing cycle. In test mode, you can advance time or manually generate invoices. Confirm that renewal charges process correctly and that access continues without interruption.
Test an upgrade flow. Move the customer from one plan to another, verifying proration is calculated correctly and that entitlements change appropriately. Check both the immediate effect and the impact on the next billing cycle.
Test a downgrade flow. Move the customer to a cheaper plan and verify the same concerns: correct proration, appropriate entitlement changes, and accurate future billing.
Simulate a payment failure. Use a test card number that declines to trigger failure handling. Verify that your application enters the appropriate state and that any grace period logic activates. Then "update" the payment method to a working test card and confirm recovery works.
Finally, cancel the subscription. Verify that cancellation processes correctly, that access is revoked at the appropriate time (immediately or at period end, depending on your policy), and that no further charges occur.
This complete lifecycle test should run automatically as part of your deployment pipeline. If any step fails, deployment should stop. Billing bugs are too expensive to catch in production.
Edge Cases You'll Inevitably Encounter
Beyond the happy path, specific edge cases deserve explicit testing because they're guaranteed to occur in production.
Concurrent operations cause race conditions that sequential testing won't catch. What happens if a customer hits "upgrade" in two browser tabs simultaneously? What if a webhook arrives while your application is still processing a related event? Test these scenarios explicitly by deliberately introducing delays and parallel requests.
Currency edge cases emerge when you support international customers. Rounding errors that seem insignificant in dollars become visible in currencies with different decimal conventions. Some currencies don't support cents at all. If you support multiple currencies, test the full lifecycle in each.
Timezone boundaries affect billing dates. A customer in Sydney who signed up at 11pm experiences a different "month" than your server running in UTC. Test subscription creation and renewal at timezone boundary times to ensure billing dates behave consistently.
Refunds bring their own complications. Full refunds should be straightforward, but partial refunds interact with proration in complex ways. What if a customer upgrades, then requests a refund for the original charge? Each scenario needs defined behaviour and explicit testing.
Expired cards during trial conversion catch many teams by surprise. A customer signs up for a free trial, their card expires during the trial, and the conversion charge fails. Your test suite should verify that trial conversion handles payment failure gracefully.
Testing Webhooks
Webhooks form the nervous system of billing integration, and they deserve dedicated testing attention. A webhook handler that mostly works will cause invisible problems when it mishandles certain event types.
First, verify webhook signature validation. Your handler should reject requests that lack valid signatures. Accepting unsigned webhooks is a security vulnerability that allows attackers to manipulate your application's billing state.
Test each webhook event type your application handles. Don't assume that handling invoice.paid correctly means invoice.payment_failed works too. The payload structures differ, and your handler logic differs. Every event type needs explicit verification.
Test out-of-order delivery. Webhooks can arrive in unexpected sequences—a subscription update event might arrive before the subscription created event. Write handlers that tolerate ordering variations, typically by fetching current state rather than assuming webhook order reflects temporal order.
Test duplicate delivery. Your payment processor might deliver the same webhook multiple times as a retry mechanism. Handlers must be idempotent: processing the same event twice should produce the same outcome as processing it once. Verify this explicitly by sending the same webhook payload twice.
Test delayed delivery. What happens if a webhook arrives hours or days late? If your handler assumes webhooks are recent, it might make incorrect decisions about current state. Use the event's embedded timestamp, not current time, when timing matters.
Staging Environment Best Practices
A staging environment that mirrors production catches issues that local testing misses. But testing billing in staging requires care to avoid cross-contamination between environments.
Use completely separate test mode credentials for staging versus development. This prevents developers from accidentally interfering with staging test data and keeps test data isolated between environments.
Populate staging with realistic test data. A single test customer doesn't exercise your billing integration the way hundreds of customers in various states will. Create customers across different plans, different lifecycle stages, and different edge case conditions.
Reset staging data periodically. Test data accumulates and becomes unrealistic over time. A weekly reset to a known baseline keeps staging useful. Automate this reset so it actually happens.
Run the full lifecycle test suite against staging before every production deployment. If it works in staging, you have reasonable confidence it'll work in production. If it fails in staging, you've caught a problem cheaply.
Monitoring Production Billing
Testing reduces risk, but monitoring catches what testing missed. Billing systems need specific monitoring beyond standard application metrics.
Track payment success rate. A sudden drop indicates a problem—whether in your integration, your payment processor, or payment methods expiring across your customer base. Set alerts for when success rate falls below historical norms.
Monitor webhook processing. Track receipt of expected webhooks and handler success rates. Missing webhooks or handler failures can signal integration problems that affect customer experience without generating obvious errors.
Reconcile subscription state. Periodically compare your application's understanding of subscription state against your payment processor's records. Discrepancies indicate synchronisation bugs that need investigation.
Watch for anomalies in billing amounts. Unexpected charges, unusual proration calculations, or pricing that doesn't match current plans can reveal bugs in your billing logic—bugs that cost you or your customers money.
These monitors should feed into alerts that reach people who can act on them. Billing anomalies discovered days later are far harder to resolve than those caught in real-time.
Salable provides a complete test mode environment that mirrors production, making it straightforward to test your entire billing integration before going live. Run your test card through the full subscription lifecycle and verify everything works before you charge a real customer.
Launching Your SaaS?
Join other founders sharing lessons on pricing, billing, and landing those first paying customers.
Related Posts
What Stripe Won't Tell You About Subscription Billing
Stripe's documentation makes subscription billing look solved. Create a product, attach a price, generate a checkout session, done. Then the cracks start showing—and you discover the iceberg beneath the surface.
Why Per-Seat Pricing Is Way Harder Than It Looks
Your first customers were individuals. But now a company wants seats for their whole team, and simple questions get complicated. Who receives the invoice? How do team members get access?
Real-Time Billing Events for Your Application
Stop polling for subscription changes. Webhooks notify your application the moment something happens, from subscription creation to payment failure.