Stripe Test Cards Beyond 4242: Production Scenarios Most Devs Skip
Every dev knows 4242 4242 4242 4242. Production then breaks on a 3DS challenge, a dispute, or a subscription dunning failure that was never tested. Here are the test cards that match what real users actually hit.
Every Stripe integration ships with the same test card. 4242 4242 4242 4242, any future expiry, any three-digit CVC. Run it through the checkout, see succeeded, deploy. Then a customer reports their payment is stuck on a loading screen, the dashboard shows a PaymentIntent in requires_action, and the frontend never knew that state existed.
This article covers the test cards worth using after 4242 works: the ones that simulate the failure modes real users hit. Three-D Secure challenges, partial captures, dispute timelines, subscription dunning, and the regional decline reasons that only appear after launch.
Quick Test Plan
Run these 10 cards through your integration before going live. About 90 minutes of work; catches most production bugs.
- 4242 4242 4242 4242 — happy path; webhook fires.
- 4000 0027 6000 3184 — 3DS challenge succeeds.
- 4000 0082 6000 3178 — 3DS challenge fails; retry UI must work.
- 4000 0000 0000 9995 — insufficient funds; specific decline code.
- 4000 0000 0000 0002 — generic decline; different message from above.
- 4000 0000 0000 0259 — auth succeeds, capture fails (in capture flow).
- 4000 0000 0000 0259 — charge succeeds, dispute fires within minutes.
- 4000 0000 0000 0341 — subscription attaches, first invoice fails.
- Webhook out of order — trigger refund before succeeded; code must not crash.
- Idempotency replay — submit twice with same key; second response equals first.
Document the result for each. Failures here are real production bugs.
The Three Categories Most Devs Miss
Stripe documents about 60 test cards. They cluster into roughly three groups, and most teams only test the first.
Synchronous successes and declines. The 4242 card and standard card_declined, insufficient_funds, expired_card family. Easy to test. Easy to handle.
Asynchronous outcomes. Cards that succeed initially but fail later, that require a second customer action (3DS), or that authorize and then fail on capture. These break naive integrations because the API response is not the final answer.
Lifecycle events. Cards that simulate disputes, subscription renewal failures, partial refunds, and reversals. These trigger webhooks days or weeks after the original transaction. Webhook handlers that are not idempotent and order-tolerant break here.
The 4242 test only validates the first category. The bugs all live in the other two.
The 3D Secure Cards You Actually Need
European Strong Customer Authentication requires 3DS challenges for most card-not-present transactions over €30 from EU-issued cards. Any integration with European customers needs a 3DS code path.
| Card number | Behavior | What it tests |
|---|---|---|
4000 0027 6000 3184 | Always requires authentication, succeeds when authenticated | The default 3DS happy path. Frontend handles requires_action. |
4000 0082 6000 3178 | Always requires authentication, fails authentication | Customer abandons after challenge. Tests retry UX. |
4000 0084 0000 0002 | Authentication required by issuer, not the regulator | Catches the bug where you only handle SCA-mandated 3DS. |
4000 0000 0000 3220 | 3DS authentication unavailable | Older cards cannot do 3DS. Tests fallback path. |
The most common bug here is handling requires_action on the PaymentIntent. The client receives a next_action object. The frontend must call stripe.confirmPayment (modern) or stripe.handleCardAction (legacy) to launch the challenge UI. Skip that step and the customer sits on a spinner indefinitely.
Disputed-Charge Test Cards
Disputes are the slowest, most painful kind of failure. The customer pays. Two weeks later the issuer sends a chargeback. Without a webhook handler that links the dispute back to original order metadata, the response is delayed and the dispute is often lost.
Three test cards simulate the lifecycle:
4000 0000 0000 0259— charges successfully, then auto-disputes within minutes. Tests thecharge.dispute.createdwebhook firing and evidence-submission code.4000 0000 0000 5126— same flow, resolves in your favor. Testscharge.dispute.closedwith statuswon.4000 0000 0000 1976— tests fraudulent disputes specifically (reason codefraudulent), which need different evidence than service disputes.
The bug to look for: when the dispute webhook fires, the system must be able to link the charge ID back to the original order or customer record. Teams that only stored the Stripe charge ID without linking find out the hard way.
Subscription Dunning Tests
Subscription revenue leaks through renewal failures: cards expire, banks flag transactions, issuers change rules. Stripe's Smart Retries try again on a schedule. Dunning logic and pause flows decide whether the customer is recovered or churned.
| Card | Scenario | What to test |
|---|---|---|
4000 0000 0000 0341 | Attaches as a payment method, fails on first invoice | Dunning email triggers; subscription enters past_due |
4000 0082 6000 3178 | Attaches; first renewal needs SCA | Off-session payment requires customer return; 3DS link in email works |
4000 0000 0000 9995 | Insufficient funds permanently | Smart Retries exhaust; subscription cancels or pauses |
4000 0000 0000 0069 | Expired card | Customer prompted to update payment method specifically |
A common production issue: dunning emails go out before the first retry, the customer panics, and support gets involved in something Stripe would have fixed itself in 24 hours. Wait until at least the second retry before sending a customer-facing email.
The Decline Codes Most Error Handlers Ignore
A common error handler:
if (error) {
showMessage("Payment failed. Please try again.");
}
This is a poor pattern. Stripe returns a decline_code field on every failed payment, and the right user message depends on the value. Some declines mean “try a different card.” Some mean “contact your bank.” Some mean “wait an hour and retry the same card.” Lumping them together trains customers to give up.
The four decline codes most worth distinguishing:
insufficient_funds— same card may work later. Suggested message: "Your card was declined. You may want to try again later or use a different card."generic_decline— bank refused, no reason given. Customer probably needs to contact their bank. Suggested message: "Your bank declined this charge. Please contact them or try a different card."fraudulent— card is reported stolen, or the issuer thinks the transaction is suspicious. Do not retry. Suggested message: "This payment was declined by your card issuer. Please use a different card."do_not_honor— bank refused for an undisclosed reason, often regional. Different card needed. Suggested message: "Your card was not accepted. Please try a different payment method."
Test cards for each: 4000 0000 0000 9995 (insufficient_funds), 4000 0000 0000 0002 (generic_decline), 4000 0000 0000 0019 (fraudulent), 4000 0000 0000 0127 (do_not_honor). For seeding QA fixtures with fresh Luhn-passing numbers across Visa, Mastercard, Amex, and Discover, the test card generator pairs well with the named Stripe scenarios above.
Authorize-Then-Capture Has Its Own Failure Mode
Manual capture flows (authorize first, capture later when the order ships) need to handle the case where the authorization succeeded but the capture fails. Card 4000 0000 0000 0259 simulates this: authorization succeeds, capture returns a decline.
This represents the real-world case where the issuer revokes the authorization between auth and capture, usually because the customer disputed the auth, the bank flagged it, or the funds moved.
The bug pattern: code that treats charge.captured as a guaranteed outcome and only handles capture failure as a generic exception. The right behavior is to release inventory, notify the customer, and prompt for a new payment method.
Webhook Race Conditions
Webhooks are not ordered. Stripe sends them at-least-once. A handler will sometimes receive charge.refunded before charge.succeeded if the network reorders them. Code that assumes ordered, sequential events has a bug.
To force this in test mode, use the Stripe CLI:
stripe trigger payment_intent.succeeded
stripe trigger charge.refunded
Run them in close succession a few times. If your code crashes, complains about an unknown charge, or marks an order as paid that was already refunded, you have found a real production bug.
The Card Numbers Are Not the Hard Part
The hard part is having a code path that handles each scenario properly. The test cards just give you a way to drive that code path on demand. The real value is the audit: when you go through the list and realize three of the ten scenarios crash your integration, that is the moment 4242 stopped being enough.
Spend 90 minutes with the right test cards before you ship. The customers you save are the ones who would have churned silently without telling you why.