TL;DR
- A declined card is the most common payment outcome your checkout will ever handle, and the one most test suites cover least.
- Failover testing checks what your system does when the first attempt fails: retry, reroute to another provider, or stop.
- Every retry path needs an idempotency check, or a flaky network turns one sale into two charges.
- Test the webhook sequence for a failed payment with the same care as a successful one, because that is where access and emails go wrong.
Most payment test plans prove that a good card works. They run the success card through checkout, confirm the order lands, and move on. The outcomes that cost real money sit on the other side of that test: a soft decline that should have been retried, a hard decline that was retried anyway, a reroute that charged twice, or a failed renewal that cancelled a customer who had money in the account.
This guide covers how to test the failure side of a payment integration: declines, retries, failover between providers, and the recovery logic that follows.
What Is Payment Failover and Why Does It Need Its Own Tests?
Payment failover is what your system does after a payment attempt fails. It has three possible answers, and each one is a separate code path:
- Retry on the same route: Try the charge again, after a delay or with changed parameters.
- Reroute to another provider: Send the same charge through a different acquirer or processor.
- Stop and recover: Mark the payment failed, tell the customer, and schedule a later attempt.
A test suite that only covers the success path never executes any of these. The logic ships untested until a real customer hits it, usually at the end of the month when renewals run.
Which Decline Types Should You Test?
Not every decline means the same thing, and your failover logic should treat them differently.
| Decline type | Example reason | Correct response | What breaks without a test |
|---|---|---|---|
| Soft decline | Insufficient funds, issuer timeout, velocity limit | Retry later or reroute | Lost sales that would have gone through on a second attempt |
| Hard decline | Stolen card, closed account, invalid number | Stop, no retry | Repeated attempts that trigger fraud flags and chargebacks |
| Authentication required | 3D Secure challenge | Present the challenge, then continue | Customers stuck on a blank step, payment left pending |
| Provider error | Gateway outage, 5xx response | Reroute or queue | Checkout down for everyone while one provider is down |
Build at least one test case per row. Provider sandboxes give you cards for most of them. The Stripe test cards guide covers Stripe's set, and other platforms publish their own. Look specifically for a decline card that still fires the failed-payment webhook, a 3D Secure challenge card, and a card that saves successfully but fails later charges. That last case is what a renewal test needs.
How Do You Test a Reroute Between Providers?
If your stack uses more than one provider, or a platform that routes across several on your behalf, the reroute is the path that matters most and is hardest to trigger on purpose.
Three things to verify:
- The reroute happens on the right declines. Force a soft decline on the primary route and confirm the charge is attempted elsewhere. Force a hard decline and confirm it is not.
- The customer sees one outcome. A reroute should be invisible at checkout. Assert that the customer gets a single success or a single failure, never an error followed by a success they did not see.
- Only one charge settles. Capture both attempts and confirm exactly one transaction ID ends up in your orders table.
On a stack you wired yourself, the test also has to cover the routing rule. On managed routing, keep the test focused on outcome and ledger: one customer-facing result, one settled charge. The routing decision stays on the provider's side.
How Do You Prove a Retry Never Double Charges?
Every retry path, whether yours or the provider's, needs the same test: the same request sent twice produces one charge.
- Send the identical request twice. Same amount, same customer, same idempotency key. Assert one transaction.
- Send it twice with a dropped response. Simulate a timeout after the first request reaches the provider, then retry. Assert one transaction.
- Send it twice with different keys. Assert two transactions. This proves the key is doing the work, and the deduplication is not an accident of timing.
# pseudocode for the double-send check
key = new_idempotency_key()
first = charge(amount=4900, customer=c1, idempotency_key=key)
second = charge(amount=4900, customer=c1, idempotency_key=key)
assert first.id == second.id
assert count_transactions(customer=c1) == 1
Run this against the sandbox, not a mock. A mock will return whatever you taught it. The sandbox returns what the provider's deduplication actually does.
What Should Happen After a Failed Payment?
The failure is not the end of the flow. It starts the recovery flow, and that is mostly webhooks.
Want AI doing the heavy lifting in your marketing?
I build the systems that handle the boring 80 percent, so you get your week back. Done properly, with the human kept in.
For a failed renewal, a complete test checks the sequence:
- The failed-payment event arrives at your endpoint with a valid signature.
- Your system records the failure without revoking access on the first attempt.
- The customer email goes out once, with the right reason.
- The scheduled retry fires on the date you configured.
- On success, the paid event arrives and access is restored. On final failure, the cancellation event arrives and access ends.
Each step is a separate assertion, and step two is the one teams get wrong. Revoking access on the first soft decline is how a customer with a temporary card problem becomes a churned customer.
If your billing platform handles retries, confirm its schedule in the sandbox and assert that your endpoint handles the event sequence in order, including out-of-order delivery. The approach in our guide to automated webhook testing applies here: replay the recorded sequence against your endpoint on every build.
Teams that bill subscriptions through a payment platform with a published sandbox, such as Whop's sandbox, can use that environment for the same decline, 3D Secure, and save-then-fail cases before they hit production renewals.
How Do You Add Failover Tests to CI?
- Tag them separately. Failover tests hit a sandbox, so they are slower than unit tests. Run them on merge to main and nightly, not on every commit.
- Record the sandbox responses. Capture the decline, reroute and retry responses once, and replay them in the fast suite. Re-record when the provider changes its API version.
- Assert on the ledger, not the UI. The question is always how many charges exist. Query the orders table or the provider's transaction list, not the confirmation page.
- Alert on the metric. In production, track the ratio of failed attempts to recovered payments. A failover path that stops working shows up there before anyone reports it.
Conclusion
The success path proves your checkout can take money. The failure path proves it can keep it. Test each decline type with its own card, force a reroute and assert a single charge, send every retry twice, and replay the full failed-payment webhook sequence against your endpoint. Put those in a nightly suite against the sandbox, and the end-of-month renewal run stops being the first time the code meets a real decline.
People Also Ask (FAQs)
Q1. What is the difference between a soft decline and a hard decline? A soft decline is temporary, such as insufficient funds or an issuer timeout, and a retry can succeed. A hard decline is final, such as a stolen or closed card, and retrying it risks fraud flags. Failover logic should retry or reroute soft declines and stop on hard ones.
Q2. How do you test payment failover without a second provider? Use a provider or platform that routes across several processors itself, then test the outcome: force a decline in the sandbox and assert that the customer sees one result and exactly one charge settles. The routing stays on the provider's side.
Q3. What is an idempotency key in payment testing? A unique value sent with a payment request so the provider can recognize a repeat of the same request and return the original result instead of charging again. Tests should send the same key twice and assert one transaction.
Q4. Should a failed renewal revoke access immediately? No. Many failed renewals are soft declines. Revoke access only after the retry schedule is exhausted and the cancellation event arrives, and test that the first failure records the event without cutting the customer off.
Q5. Can failover tests run in a sandbox? Yes, and they should. Provider sandboxes supply decline, authentication and save-then-fail test cards, and fire the same webhooks as production, so the full sequence can be recorded and replayed in CI.

