Every Stripe integration that goes to production has a moment where someone says: “wait, did we test the dispute flow?”
Usually no. Disputes are hard to trigger on purpose. So is the 3D Secure challenge. So is a delayed payment_intent failure that arrives 4 seconds after your handler thought the charge was done. So is a refund webhook arriving twice because Stripe retried the delivery.
The result is that most teams ship the happy path, hope the rest works, and find out about the edge cases when a real customer hits them.
This post is about the 9 scenarios you should actually test, and how to test them without touching production keys.
Why Stripe's test mode is not enough
Stripe's test mode is great for what it does. Use it for end-to-end checkout flows, real card capture, and the parts of your integration that need to talk to Stripe's actual infrastructure.
What test mode is not great at:
- Triggering specific failure events on demand (some are nearly impossible to fire)
- Simulating webhook retries and out-of-order delivery
- Running the same test repeatedly in CI without sharing state with your team
- Onboarding a new engineer without giving them a Stripe test API key
- Replaying a specific webhook event 10 times to test idempotency
For all of this, you need something more controllable. We'll come back to that.
The 9 scenarios
1. payment_intent.succeeded — the happy path
Yes, you should still test this one. Specifically, you should test that:
- Your handler verifies the Stripe signature (don't skip this in tests)
- Your handler is idempotent — run it twice with the same event, confirm only one fulfillment record gets created
- Your handler returns 2xx within Stripe's timeout window
2. payment_intent.payment_failed — declined card
The most common failure mode in production. Your test should verify:
- You correctly extract the decline reason from
last_payment_error.decline_code - You don't accidentally fulfill the order
- You surface a useful error to the customer (don't leak Stripe internals)
Common decline codes worth testing individually: card_declined, insufficient_funds, expired_card, incorrect_cvc, processing_error.
3. 3D Secure / Strong Customer Authentication (SCA)
Required by PSD2 in the EU, increasingly common globally. Your handler must correctly process payment_intent.requires_action events and resume the flow when the customer completes the challenge.
The hard part about testing this against real Stripe is that you need to actually click through a 3DS challenge in a browser. In a sandbox, you can fire the events directly.
4. charge.refunded — partial and full refunds
Your test should cover:
- Full refund — verify your fulfillment status flips to refunded
- Partial refund — verify the amount delta is computed correctly
- Double refund — Stripe will not allow this, but make sure your handler returns a clean error if your code somehow tries
5. charge.dispute.created — chargebacks
The scariest webhook because it has real money implications. Test that:
- You correctly receive the dispute and pause any pending fulfillment for the customer
- You record the dispute amount and reason for downstream reporting
- Your handler doesn't accidentally credit the customer twice (once for the dispute, once for a manual refund)
6. customer.subscription.updated — plan changes and proration
For SaaS billing. Test that you correctly compute proration when the customer upgrades or downgrades mid-cycle.
7. invoice.payment_failed — dunning
Your retry policy is important. Test that you don't accidentally cancel a subscription on the first failure, that you correctly count attempts, and that you eventually do cancel after your configured threshold.
8. Rate-limited responses (429)
Stripe will rate-limit you in production if you batch too aggressively. Your client should respect Retry-After headers and back off. Test this by configuring your sandbox to return 429 on N consecutive requests.
9. Webhook retries and duplicate delivery
Stripe will retry a webhook delivery if your endpoint returns non-2xx. It will also occasionally deliver the same event twice even if your endpoint returned 200. Your handler must be idempotent on the event.id.
Test it: replay the same webhook event 10 times against your handler and assert that downstream effects (database rows, emails sent, fulfillment records) only happened once.
How to run these scenarios safely
You have three real options:
Option 1: Stripe test mode + manual triggering. Works for the basic scenarios. Hard or impossible for disputes, delayed failures, retry simulation, and 3DS. Requires a Stripe account. Burns through test events that count toward dashboard clutter.
Option 2: Hand-rolled mock server. Fast to start, drifts within a quarter, can't simulate state transitions or out-of-order delivery, and tells you nothing about whether your flow actually works end-to-end.
Option 3: A stateful Stripe sandbox. Spin up a fresh isolated environment per test, fire any of the 9 webhooks on demand, replay events for idempotency tests, and use it in CI without giving the runner a real Stripe key.
That third option is what we built FetchSandbox for. The Stripe sandbox page covers all 21 Stripe resources and includes scenarios for every webhook in this post. The Stripe docs portal has runnable examples for every endpoint and a download link for an agent-ready integration guide if you want Claude Code or Cursor to write the handler for you.
The minimum-viable webhook test in 4 lines
If you want to copy-paste something into your codebase right now:
curl -X POST https://fetchsandbox.com/api/sandboxes \
-H "Content-Type: application/json" \
-d '{"spec_id":"stripe"}'
# Returns: { id, base_url, credentials: [{ api_key }] }Now point your Stripe client at the sandbox URL, send a charge, and watch your webhook handler receive the events. No production keys, no real card data, no Stripe account required.
Why this matters more than people think
Most production payment bugs are not in the happy path. They are in the second event, the retried delivery, the out-of-order callback, the dispute that arrives 6 days after the original charge, the refund that double-fires because the agent double-clicked.
If your test suite only covers payment_intent.succeeded, you have a half-tested integration. The real test is whether your handler can survive every weird thing Stripe will eventually throw at it.
Two related reads if you want to go deeper: stateful sandbox vs mock server covers why most testing tools can't actually do this, and why API integrations break after the first successful request is the longer argument about why workflow testing matters more than endpoint testing.