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.
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. Whop's sandbox, for example, has a card that declines and still fires the failed-payment webhook, a card that triggers a 3D Secure challenge, and one that saves successfully but fails every later charge, which is the exact case 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 didn't see.
- Only one charge settles. Capture both attempts and confirm exactly one transaction ID ends up in your orders table.
On platforms that handle routing for you, the reroute is a setting. Whop routes each payment across its partner providers and resends a declined transaction through an alternative in real time when orchestration is switched on, so your test asserts the outcome and the single charge, and the routing itself stays on the provider's side. On a stack you've wired yourself, the test also has to cover the routing rule.
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 isn't 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.
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: Whop, for example, lists retries under its billing automation option, so the test is that your endpoint handles the sequence of events 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.
How Do You Add Failover Tests to CI?
- Tag them separately. Failover tests hit a sandbox, so they're 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.







