Stripe publishes a set of card numbers that only exist inside its test environment. Each one triggers a specific, documented outcome — an approval, a particular decline, a 3-D Secure challenge — so you can exercise every branch of your payment code without a bank being involved.
They are not generic dummy numbers. They are recognised by Stripe and by nothing else, which is exactly what makes them useful and exactly why numbers from anywhere else, including our generator, do not work with them.
This page is the deep version. For the same numbers alongside PayPal, Braintree, Adyen, Square, and Authorize.Net, the test card numbers reference is the cross-gateway summary, and the PayPal sandbox guide is the equivalent deep dive for PayPal and Braintree — which select outcomes by cardholder name and transaction amount rather than by card number.
Test mode and live mode
Stripe environments are selected by the API key, not by the request:
| Test mode | Live mode | |
|---|---|---|
| Secret key | sk_test_… | sk_live_… |
| Publishable key | pk_test_… | pk_live_… |
| Test cards | Recognised | Rejected |
| Real cards | Rejected | Charged |
| Money moves | No | Yes |
The Dashboard has a toggle that switches which set of data you are looking at; the API has no such toggle, only the key. This single fact explains most confused bug reports in this area — “the test card stopped working” is almost always a live key in the environment, and “my real card was declined in staging” is the same mistake in the other direction.
On key handling: a test key is lower-risk than a live key, not zero-risk. It can read your test data, create objects, and in some integrations reveal your business structure. Keep both out of the repository, out of client-side bundles, and out of CI logs. Stripe’s API keys documentation covers rotation and restricted keys, which are worth using for anything running unattended.
Success cards by brand
Any future expiry and any CVC of the correct length work with all of these. The number alone determines the outcome.
| Card number | Brand | CVC | PaymentMethod token |
|---|---|---|---|
| 4242 4242 4242 4242 | Visa | 3 digits | pm_card_visa |
| 4000 0566 5566 5556 | Visa (debit) | 3 digits | pm_card_visa_debit |
| 5555 5555 5555 4444 | Mastercard | 3 digits | pm_card_mastercard |
| 2223 0031 2200 3222 | Mastercard (2-series) | 3 digits | — |
| 5200 8282 8282 8210 | Mastercard (debit) | 3 digits | pm_card_mastercard_debit |
| 5105 1051 0510 5100 | Mastercard (prepaid) | 3 digits | pm_card_mastercard_prepaid |
| 3782 822463 10005 | American Express | 4 digits | pm_card_amex |
| 6011 1111 1111 1117 | Discover | 3 digits | pm_card_discover |
| 3056 9300 0902 0004 | Diners Club | 3 digits | pm_card_diners |
| 3566 0020 2036 0505 | JCB | 3 digits | pm_card_jcb |
| 6200 0000 0000 0005 | UnionPay | 3 digits | pm_card_unionpay |
The 2-series Mastercard is the one to keep in your fixtures deliberately. Brand detection written before 2017 classifies it as unknown, and it is the single most common card-type bug still shipping — the BIN and IIN guide explains why that range exists.
The right-hand column matters for server-side tests. Raw card numbers in your backend code
put that code in PCI scope; the pm_card_* tokens produce the same results without a PAN
ever touching your server, so prefer them anywhere you are not specifically testing the
input form.
Decline cards
This is the section worth copying into a fixture file. Each number produces a real error object, not a simulated one.
| Card number | Error code | Decline code |
|---|---|---|
| 4000 0000 0000 0002 | card_declined | generic_decline |
| 4000 0000 0000 9995 | card_declined | insufficient_funds |
| 4000 0000 0000 9987 | card_declined | lost_card |
| 4000 0000 0000 9979 | card_declined | stolen_card |
| 4000 0000 0000 6975 | card_declined | card_velocity_exceeded |
| 4000 0000 0000 0069 | expired_card | — |
| 4000 0000 0000 0127 | incorrect_cvc | — |
| 4000 0000 0000 0119 | processing_error | — |
| 4242 4242 4242 4241 | incorrect_number | — |
That last number deliberately fails the Luhn checksum. It exists to test the branch where your own validation should have rejected the input before Stripe ever saw it — if it reaches the API, your client-side check has a gap.
Never show a decline code to the customer
lost_card and stolen_card are the reason this deserves its own heading.
Those codes are information from the issuer to the merchant. Displayed to the person at the checkout, they are either wrong — plenty of legitimate cards decline for reasons the code describes badly — or actively harmful, because telling someone holding a card that it is reported stolen is a safety problem, and telling a fraudster the same thing is a free diagnostic.
Map every decline to one of two customer-facing messages: try a different payment method, or contact your bank. Log the real code for yourself. Stripe’s decline codes reference documents which are retriable and which are not — that distinction belongs in your retry logic, not in your copy.
3-D Secure test cards
| Card number | Behaviour |
|---|---|
| 4000 0000 0000 3220 | Always requires a 3DS2 challenge, then succeeds |
| 4000 0027 6000 3184 | Requires authentication on all transactions |
| 4000 0084 0000 1629 | Requires authentication, then declines afterwards |
| 4000 0025 0000 3155 | Requires authentication unless set up for off-session use |
| 4000 0000 0000 3055 | Supports authentication but does not require it |
| 4242 4242 4242 4242 | Supports 3DS but is not enrolled — no challenge appears |
| 3782 822463 10005 | No 3DS support at all |
4000 0084 0000 1629 is the important one. Code that treats a completed challenge as a
completed payment breaks here, and that assumption is common enough that it is worth an
explicit test. Authentication proves who the cardholder is; it does not commit the issuer
to approving anything.
The 3-D Secure testing guide covers the flow
in full.
Testing individual fields
- Expiry — any future month and year. To exercise your own expiry validation, a past date is rejected client-side before Stripe is involved; to see the API’s
expired_carderror, use4000 0000 0000 0069with a future date. - CVC — any three digits, four on American Express. Omitting it entirely makes Stripe skip the check, which means a CVC test that passes with no CVC sent is not testing anything.
- Postal code and AVS — any value succeeds by default; Stripe documents specific cards for triggering address and postal-code check failures. As with CVC, omitted values are skipped rather than failed.
- Cardholder name — free text on Stripe, unlike PayPal’s sandbox where the name selects the outcome.
Beyond cards
Card testing is not the whole surface. Stripe documents separate test values for SEPA Direct Debit, iDEAL, Bancontact, ACH direct debit, and the rest of its payment method catalogue — each with its own success and failure identifiers. If your checkout offers anything other than cards, those paths need the same treatment; the testing documentation has a method selector at the top of the page for exactly this.
Testing webhooks
Webhooks are where integrations most often break in production while passing every test, because the local development loop skips them entirely.
The Stripe CLI closes that gap. It forwards real test-mode events to a local port and prints a signing secret scoped to that session:
stripe listen --forward-to localhost:3000/webhook
# → Ready! Your webhook signing secret is whsec_... (^C to quit)
# In another terminal, fire a specific event on demand:
stripe trigger payment_intent.succeeded
Verify the signature on every request. The raw request body is required — parsed JSON will
not verify, which is the most common cause of a handler that works with stripe trigger
and fails against real deliveries:
import Stripe from 'stripe';
import express from 'express';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();
app.post(
'/webhook',
express.raw({ type: 'application/json' }), // raw body, not express.json()
(req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook signature failed: ${err.message}`);
}
// Stripe retries on non-2xx and can deliver the same event twice.
// Record event.id and return early if you have already handled it.
if (alreadyHandled(event.id)) return res.json({ received: true });
if (event.type === 'payment_intent.succeeded') {
fulfil(event.data.object);
}
markHandled(event.id);
res.json({ received: true });
}
);
Two properties to test explicitly: a tampered signature must be rejected, and the same event delivered twice must fulfil the order once. Stripe’s webhook documentation sets out the delivery and retry guarantees you are coding against.
Test clocks for subscriptions
Subscription bugs live in the future — a renewal that fails, a trial that converts, a dunning sequence that never fires. Waiting a month to find out is not a test strategy.
Test clocks let you attach a
customer to a simulated clock and advance it. A year of billing cycles runs in a few
seconds, with real invoices, real webhook events, and real failure behaviour. Combine one
with 4000 0000 0000 9995 on the second renewal and you can watch your dunning flow work
before a customer ever meets it. It is the least-used feature in this list and the one that
finds the most bugs.
Common mistakes
- Test card against a live key. The card is fine; the key is wrong. Check which key the environment actually loaded before debugging anything else.
- Sending a generated number to Stripe. It passes your form and dies at the API, because no issuer stands behind it — why test cards fail on real systems covers the mechanism.
- Only testing the happy path. The decline table exists so that your error handling is exercised. A checkout tested only with
4242…has never run its own failure branch. - Not testing webhooks. Your fulfilment logic almost certainly lives there. Skipping it means the least-tested code owns the most important step.
- Trusting the client-side confirmation. Fulfil on
payment_intent.succeededfrom a verified webhook, not on the browser reporting success — the browser can close, lie, or be replayed. - Never testing 3DS. Authentication is mandatory in the EEA and increasingly common elsewhere. An untested challenge flow is an untested checkout.
For the input layer that sits in front of all of this — field lengths, brand detection, masking, and bulk fixtures from the bulk generator — the payment form testing checklist covers what to assert before a request ever reaches Stripe.
The card numbers, error codes and CLI commands above were last checked against provider documentation on . Providers do change what they publish — the official link beside each claim is authoritative.