Testing Payment Flows: Stripe, Webhooks, and Edge Cases

Veld Systems||8 min read

Payment integrations are the most consequential code in any SaaS application. When authentication breaks, users cannot log in. When a dashboard breaks, users see an error page. When payment code breaks, you either lose revenue or accidentally charge customers the wrong amount. Both outcomes are significantly worse than a broken UI component.

Despite this, payment flows are consistently the least tested part of most SaaS codebases. We have audited projects where the checkout flow had zero automated tests, the webhook handler had no error handling, and the team's testing strategy was "use a test card in staging and hope for the best." This post covers how to properly test Stripe payment integrations, including the edge cases that most teams discover only in production.

Why Payment Testing Is Hard

Payment flows are difficult to test because they span multiple systems and involve asynchronous events that arrive out of order, sometimes not at all.

A typical payment flow involves: your frontend collecting card details via Stripe Elements, Stripe creating a PaymentIntent, your backend confirming the payment, Stripe sending webhook events for each state transition, your webhook handler updating your database, and your frontend reflecting the new state. That is six distinct steps across three systems (your frontend, your backend, Stripe), any of which can fail independently.

Add to this the fact that Stripe webhook events can arrive out of order, can be duplicated, can be delayed by minutes, and can fail delivery entirely. Your webhook handler needs to be idempotent, handle every event type your integration uses, and gracefully handle events that reference objects your system does not know about.

The complexity is real. But it is also finite and well understood. The edge cases are enumerable, and a structured testing approach covers them.

Setting Up Your Test Environment

Before writing tests, get your test infrastructure right.

Stripe test mode. Every Stripe account has a test mode with separate API keys, separate webhook endpoints, and a full set of test card numbers that simulate different outcomes. Use test mode for all development and automated testing. Never run automated tests against your live Stripe keys.

Stripe CLI for local webhook testing. The Stripe CLI can forward webhook events to your local development server. Run `stripe listen --forward-to localhost:3000/api/webhooks/stripe` and your local server receives real Stripe webhook payloads. This is essential for developing and debugging webhook handlers.

Stripe test clocks. For subscription testing, Stripe offers test clocks that let you simulate the passage of time. You can create a subscription, advance the clock by a month, and trigger a renewal event without waiting 30 days. This is how you test billing cycle transitions, trial expirations, and past due recovery flows.

Fixture based tests. Capture real Stripe webhook payloads during development and save them as test fixtures. Use these fixtures in unit tests for your webhook handler. This lets you test handler logic without hitting Stripe's API, which makes tests fast and deterministic.

Testing the Checkout Flow

The checkout flow has three testing layers.

Unit tests for pricing logic. If your application calculates prices, applies discounts, or determines tax, test that logic in isolation. Feed it known inputs and verify outputs. Common edge cases: applying percentage discounts to amounts that produce fractional cents, stacking multiple discounts, handling currency specific rounding rules, and zero dollar amounts.

Integration tests for Stripe API calls. Test that your backend correctly creates Stripe Checkout Sessions, PaymentIntents, or Subscriptions with the right parameters. Use Stripe's test mode API for these tests. Verify that:

- The correct amount and currency are sent to Stripe

- Metadata is attached (customer ID, plan ID, internal references your webhook handler will need)

- Success and cancel URLs are configured correctly

- Tax settings and coupon codes are applied when present

End to end tests for the full flow. Automate the complete checkout flow: user selects a plan, enters test card details, completes checkout, and arrives at the success page with their account upgraded. Use Stripe's test card `4242 4242 4242 4242` for the happy path. These tests are slow (5 to 10 seconds each) but they verify the entire chain works.

Testing Webhook Handlers

Webhook handler testing is where most teams under invest, and where the worst production bugs originate.

Test every event type you handle. If your handler processes `checkout.session.completed`, `invoice.paid`, `invoice.payment_failed`, `customer.subscription.updated`, and `customer.subscription.deleted`, you need tests for each one. Use saved fixture payloads and verify the correct database state after each event is processed.

Test idempotency. Send the same webhook event twice and verify the handler produces the same result without duplicating records. Stripe can and does send duplicate events. If your handler creates a subscription record on `checkout.session.completed`, sending that event twice should not create two subscription records. Implement this by checking for existing records keyed on the Stripe event ID or the Stripe object ID before creating new ones.

Test out of order delivery. Stripe does not guarantee webhook event order. Your handler might receive `invoice.paid` before `checkout.session.completed`, or `customer.subscription.deleted` before `customer.subscription.updated`. Test these scenarios explicitly. The safest pattern is to treat each event as a state assertion ("the subscription is now active") rather than a state transition ("move the subscription from pending to active").

Test signature verification. Stripe signs webhook payloads with a secret. Your handler should verify this signature before processing any event. Test that requests with invalid signatures are rejected with a 400 status code. Test that requests with missing signature headers are rejected. This prevents spoofed webhook calls from modifying your data.

Test failure and retry behavior. When your webhook handler throws an error, Stripe retries the event. Test that transient failures (database connection timeout, downstream service unavailable) result in your handler returning a 500 so Stripe retries, while permanent failures (malformed payload, unknown event type) return a 200 to prevent infinite retries.

The Edge Cases Most Teams Miss

These are the scenarios we have seen cause real production issues. Every one of them should have an automated test.

Card declined after initial success. The user completes checkout with a card that passes the initial charge but fails on the first renewal. Your system needs to handle the `invoice.payment_failed` event by flagging the account, sending a dunning email, and eventually suspending access if the payment is not recovered. Test the full sequence: successful checkout, advance the test clock one billing period, trigger a payment failure, verify the account state.

Subscription upgrade mid cycle. When a user upgrades from a monthly plan to a more expensive monthly plan mid cycle, Stripe handles proration. Test that your system correctly reflects the new plan immediately while Stripe handles the billing adjustment on the next invoice.

Subscription downgrade behavior. Downgrades can be configured to take effect immediately or at the end of the current billing period. Test both scenarios and verify your system reflects the correct plan at the correct time.

Customer dispute (chargeback). When a customer files a dispute, Stripe sends a `charge.dispute.created` event. Test that your system receives it, flags the account, and triggers your dispute response workflow. Chargebacks cost $15 per dispute on top of the refunded amount, so automated handling matters.

Expired payment methods. A customer's card expires and their next payment fails. Test the dunning sequence: initial failure notification, retry attempts (Stripe's Smart Retries), and eventual subscription cancellation if the card is not updated.

Webhook endpoint downtime. If your webhook endpoint is down for an hour, Stripe queues events and retries. Test that your system correctly processes a burst of queued events when it comes back online, handling them idempotently even if they arrive in non chronological order.

Currency edge cases. If you accept multiple currencies, test that zero decimal currencies (like JPY) are handled correctly. Stripe represents amounts in the smallest currency unit, so 1000 JPY is sent as `1000`, while $10.00 USD is sent as `1000`. Getting this wrong means charging 100x too much or too little.

Monitoring and Alerting

Testing catches bugs before deployment. Monitoring catches bugs that slip through and configuration issues that only manifest in production.

Webhook delivery monitoring. Stripe's dashboard shows webhook delivery success rates. Set up an alert if the success rate drops below 99%. A failing webhook endpoint means your system is falling out of sync with Stripe.

Revenue reconciliation. Run a daily job that compares your database's subscription records against Stripe's records via the API. Any discrepancy, a subscription that exists in Stripe but not in your database, or vice versa, should trigger an alert. We have seen webhook handlers silently fail for specific event types while appearing healthy overall. Reconciliation catches this.

Payment failure rate monitoring. Track the percentage of failed payments over time. A sudden spike might indicate a platform issue, a card testing attack, or a problem with your checkout flow. Normal failure rates are 2 to 5% for card payments. Anything above 10% warrants investigation.

Our payment processing guide covers the broader architecture for handling payments in production, including the monitoring and reconciliation patterns that keep your billing system trustworthy.

The Minimum Viable Payment Test Suite

If you are starting from zero test coverage on your payment integration, here is the priority order:

1. Webhook signature verification (prevents spoofed events, 1 hour to implement)

2. Happy path checkout to subscription activation (end to end, covers the core flow)

3. Idempotent webhook handling (prevents duplicate records, saves countless support tickets)

4. Payment failure and dunning sequence (protects revenue, catches the most common post launch issue)

5. Subscription upgrade and downgrade (prevents plan confusion and billing errors)

These five test areas cover roughly 90% of real payment bugs. The remaining 10% are the currency edge cases, dispute handling, and timing scenarios that you add as your payment volume grows.

Building a bulletproof payment integration is something we have done across multiple projects, from early stage startups to platforms processing millions in annual recurring revenue. This is a core part of the full stack development work we deliver. If you are launching with Stripe and want your payment code tested properly from day one, or if you have an existing integration that needs hardening, get in touch. Payments are not the place to learn by making mistakes in production.

For teams evaluating their Stripe setup against alternatives, our Stripe vs Square comparison covers the platform differences. And if you are building the broader billing architecture around Stripe, our post on subscription billing architecture covers the patterns that sit around the payment integration itself.

Ready to Build?

Let us talk about your project

We take on 3-4 projects at a time. Get an honest assessment within 24 hours.