Advertisement

Testing payments

How to test a Stripe checkout end to end

A Stripe integration is only as reliable as the failure paths you have tested. This guide walks through a complete test plan for a Stripe checkout: the environment and keys, the documented test cards for success and every common decline, 3D Secure, Radar, webhooks and the checks to run before you switch to live keys.

By the Fakecreditcard teamPublished 9 min read

Advertisement

Key takeaways

  • Test in a Stripe sandbox with pk_test_ / sk_test_ keys. Stripe’s services agreement prohibits testing in live mode with real card details.
  • Use the documented test cards in the browser and test PaymentMethod IDs such as pm_card_visa in server-side code and automated tests.
  • Fulfill orders from webhooks, not from the redirect. Test your handler locally with stripe listen --forward-to and stripe trigger.
  • 3D Secure only appears with the 3DS test cards and only in your own integration — payments created in the Dashboard never redirect to 3DS.
  • Before launch, swap and rotate keys, register live webhook endpoints, and make sure your handler copes with duplicate and out-of-order events.
On this page
  1. Set up a safe test environment
  2. Test the happy path with test cards
  3. Simulate declines and card errors
  4. Test 3D Secure and requires_action
  5. Test Radar and fraud rules
  6. Test webhooks locally with the Stripe CLI
  7. Make retries safe with idempotency keys
  8. Go-live checklist
  9. Frequently asked questions

Most checkout bugs don’t show up on the happy path. They appear when a card is declined, when the bank asks for 3D Secure, when a webhook arrives twice, or when the customer closes the tab before the redirect. Stripe gives you a full set of tools to reproduce each of those situations safely. This guide puts them together into one test plan you can run before every release.

Set up a safe test environment

Stripe separates test activity from live activity. You test in a sandbox — an isolated test environment you select from the account picker in the Dashboard — and every API call uses that environment’s test keys. Test transactions never move money, and objects you create there (products, prices, coupons, customers) are not available in live mode.

KeyPrefixWhere it lives
Publishable keypk_test_Browser or mobile app (Stripe.js, Elements, mobile SDKs)
Secret keysk_test_Your server only, loaded from an environment variable or secret manager
Webhook signing secretwhsec_Your server, one per endpoint (and a separate one per environment)

Two rules matter more than any other. First, never use real card details: Stripe’s testing documentation says so explicitly, and its services agreement prohibits testing in live mode with real payment method details. Second, keep keys out of your code. Load them from configuration so that the switch from test to live is a configuration change, not a code change — and so that a test key can never reach production by accident. Stripe also notes that you can’t process live payments while an integration still uses test keys.

Why not use generated numbers here?

A random Luhn-valid number from a card generator is fine for testing your own form validation, but Stripe’s test environment simulates outcomes only for its documented test cards. Use generated numbers before the payment request and Stripe’s numbers from the moment you call the API.

Test the happy path with test cards

Start with a successful payment for every card brand you accept. Each test card works with any future expiry date, such as 12/34, and any three-digit CVC (four digits for American Express). Other fields, such as the name or postal code, can be any value.

BrandTest numberWhy test it
Visa4242 4242 4242 4242Baseline success
Visa (debit)4000 0566 5566 5556Debit funding type in your reporting
Mastercard5555 5555 5555 4444Classic 51–55 range
Mastercard (2-series)2223 0031 2200 3222Catches brand detection that only knows 51–55
American Express3782 822463 1000515 digits, 4-digit CID, 4-6-5 grouping
Discover6011 1111 1111 11176011 prefix
Diners Club (14-digit)3622 720627 1667Shortest common length
JCB3566 0020 2036 05053528–3589 range
UnionPay (19-digit)6205 5000 0000 0000 004Longest length; input masks often cut it off

For each card, confirm the whole chain rather than just the success screen: the PaymentIntent reaches succeeded, your webhook handler receives payment_intent.succeeded, the order is marked paid exactly once, and the confirmation email or receipt goes out. The full list, including co-branded cards, is on our Stripe test cards page.

Raw numbers in the browser, PaymentMethod IDs on the server

Type the raw numbers into Elements, Checkout or Payment Links, the same way a customer would. In server-side code and automated tests, Stripe recommends test PaymentMethods such as pm_card_visa instead of card numbers, warning that code which sends raw card numbers to the API might not be PCI-compliant when it goes live.

Node.js — automated test with a test PaymentMethod
const Stripe = require('stripe');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // sk_test_... from config

test('declined card leaves the order unpaid', async () => {
  const order = await createOrder({ amount: 2500, currency: 'usd' });
  await expect(
    stripe.paymentIntents.create({
      amount: order.amount,
      currency: order.currency,
      payment_method: 'pm_card_visa_chargeDeclined',
      confirm: true,
      automatic_payment_methods: { enabled: true, allow_redirects: 'never' },
    })
  ).rejects.toMatchObject({ type: 'StripeCardError', code: 'card_declined' });
  expect((await getOrder(order.id)).status).toBe('pending');
});
Advertisement

Simulate declines and card errors

Declines are where real customers get stuck, so test each one your UI distinguishes. Stripe documents a test card for each common error and decline code:

ScenarioCardResultPaymentMethod
Generic decline4000 0000 0000 0002card_declined / generic_declinepm_card_visa_chargeDeclined
Insufficient funds4000 0000 0000 9995card_declined / insufficient_fundspm_card_visa_chargeDeclinedInsufficientFunds
Expired card4000 0000 0000 0069expired_cardpm_card_chargeDeclinedExpiredCard
Incorrect CVC4000 0000 0000 0127incorrect_cvcpm_card_chargeDeclinedIncorrectCvc
Processing error4000 0000 0000 0119processing_error—
Lost / stolen card4000 0000 0000 9987card_declined / lost_card—
Attaches, then fails when charged4000 0000 0000 0341Attach succeeds, charge failspm_card_chargeCustomerFail

Three details trip up many test plans:

  • Cards that simulate issuer declines can’t be attached to a Customer. If you save cards first and charge later (subscriptions, one-click checkout), use 4000 0000 0000 0341: attaching succeeds and the later charge fails, which is exactly the renewal-failure path you need to test.
  • Client-side validation errors are separate. An invalid month such as 13 returns invalid_expiry_month, and a two-digit CVC such as 99 returns invalid_cvc. A 4242 number with a wrong last digit fails the Luhn check before any decline logic runs.
  • Map codes to helpful messages. “Your card has insufficient funds” and “Your card’s security code is incorrect” lead to different customer actions. Don’t show raw decline codes such as lost_card to the customer; a neutral “Your card was declined” is the safer message.

Test 3D Secure and requires_action

When a bank requires authentication, the PaymentIntent moves to requires_action and your front end must display the 3D Secure step. Stripe.js shows the authentication UI in a modal automatically when you confirm with its methods; if you control the flow yourself, you read next_action and redirect or open an iframe. In a sandbox Stripe shows a mock authentication page where you can complete or fail the challenge.

CardBehavior in Stripe’s docs
4000 0027 6000 3184Requires authentication on all transactions, regardless of how the card is set up
4000 0025 0000 3155Requires authentication for off-session payments unless the card was set up for future use; on-session payments always require it
4000 0084 0000 16293DS required, then declined with card_declined after authentication
4000 0000 0000 30553DS supported but not required
3782 822463 100053DS not supported; the payment proceeds without authentication

Run these through your real checkout, not the Dashboard: Stripe states that 3D Secure redirects won’t occur for payments created directly in the Dashboard. Only the cards in Stripe’s 3D Secure section reliably exercise authentication — other test cards may still trigger 3DS but return attempt_acknowledged to skip the extra steps. For mobile apps, Stripe also documents cards that force each challenge UI (out-of-band, one-time passcode, single select and multi select). Our 3-D Secure testing guide covers the outcomes in more depth and compares gateways.

Test Radar and fraud rules

If you rely on Radar, test what your checkout does when a payment is blocked or flagged. Blocked payments return a card error with the code fraud, and your UI should handle it like a decline without revealing why.

CardRadar behavior
4100 0000 0000 0019Risk level “highest”; Radar always blocks it (pm_card_radarBlock)
4000 0000 0000 4954Risk level “highest”; may be blocked depending on your settings
4000 0000 0000 9235Risk level “elevated”; may be queued for review
4000 0000 0000 0101CVC check fails if you provide a CVC
4000 0000 0000 0036Postal code check fails if you provide a postal code

Remember that Stripe skips CVC and postal code checks when the field is empty, so those checks can’t fail unless you send a value. You can also write custom Radar rules in your test environment to trigger 3D Secure on specific test cards. For a merchant’s view of automated fraud, see our guide to stopping card-testing attacks.

Test webhooks locally with the Stripe CLI

The redirect after payment is not a reliable signal: customers close tabs, networks drop, and some payment methods confirm minutes or days later. Stripe recommends driving fulfillment from webhooks. To receive events on your laptop without a public URL, use the Stripe CLI:

Shell
stripe login
stripe listen --forward-to localhost:4242/webhook
# Ready! Your webhook signing secret is 'whsec_...' (^C to quit)

# in a second terminal
stripe trigger payment_intent.succeeded
stripe trigger payment_intent.payment_failed

Put the printed whsec_ secret in your local configuration and verify every request with Stripe’s library, using the raw request body and the Stripe-Signature header. Frameworks that parse JSON before your handler runs break the signature check, which is one of the most common local-testing failures.

Node.js — Express webhook handler
app.post('/webhook', express.raw({ type: 'application/json' }), async (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('Invalid signature');
  }
  if (await alreadyProcessed(event.id)) return res.sendStatus(200); // duplicate delivery
  await queue.add(event);                                              // do the slow work later
  res.sendStatus(200);
});

What to verify in your handler

  • Fast 2xx responses. Return a success status before slow work such as emails or accounting updates; Stripe treats timeouts and redirects as failed deliveries.
  • Duplicates. The same event can arrive more than once. Log processed event IDs and skip repeats.
  • Ordering. Stripe doesn’t guarantee delivery order, so don’t assume invoice.created arrives before invoice.paid. Fetch the object from the API if you need its latest state.
  • Retries. In live mode Stripe retries failed deliveries for up to three days; sandbox events are retried three times over a few hours. You can resend an event with stripe events resend.

Make retries safe with idempotency keys

A network timeout after you send a payment request leaves you not knowing whether the payment was created. Stripe’s API solves this with the Idempotency-Key header: send the same key when you retry and Stripe returns the saved result of the first request instead of creating a second payment. All v1 POST requests accept keys of up to 255 characters; Stripe suggests V4 UUIDs and says keys can be removed once they are at least 24 hours old. If a retry reuses a key with different parameters, Stripe returns an error.

Test this deliberately: send the same create request twice with one key and assert that only one PaymentIntent exists. Then double-click your own “Pay” button and confirm the order is charged once.

Go-live checklist

Stripe designed its sandbox and live environments to behave as similarly as possible, so going live is mostly a key swap. Most launch-day problems come from the items around it:

  1. Rotate and secure keys. Rotate keys that may have been saved outside your codebase during development, and make sure no key is committed to the repository.
  2. Register live webhook endpoints. Test and live endpoints are separate and have different signing secrets. Confirm the live endpoint behaves exactly like the test one.
  3. Recreate test objects. Products, prices and coupons from the sandbox don’t exist in live mode. Recreate them with the IDs your code expects.
  4. Pin the API version. Set the API version in your library so behavior doesn’t change underneath you.
  5. Review error handling and logs. Distinguish card errors (show the customer a message) from your own request errors (alert your team), and check that your logs never store card numbers.
  6. Re-run the edge cases. Incomplete data, invalid data and duplicate requests, ideally tested by someone who didn’t write the code.

Finally, keep real card data out of every non-production system — logs, fixtures, screenshots and staging databases. Our guide to test data and PCI DSS explains why and how.

Frequently asked questions

What is the basic Stripe test card number?

4242 4242 4242 4242 (Visa) with any future expiry date, such as 12/34, and any three-digit CVC. It succeeds in a sandbox and is the usual starting point for the happy path.

Can I test Stripe with a real card?

No. Stripe’s testing documentation says not to use real card details, and its services agreement prohibits testing in live mode with real payment method details. Use test keys and the documented test cards.

Why does my CVC or postal code check never fail in test mode?

Stripe skips those checks when the field is empty. Send a CVC with 4000 0000 0000 0101 or a postal code with 4000 0000 0000 0036 and the check fails.

How do I test Stripe webhooks on localhost?

Install the Stripe CLI, run stripe login, then stripe listen --forward-to localhost:4242/webhook. The command prints a signing secret for your handler. Trigger events with stripe trigger payment_intent.succeeded or by running real test payments.

Why do I get HTTP 429 errors in my Stripe tests?

The rate limiter is stricter in test environments than in live mode. Reduce parallelism in your test suite and don’t use a sandbox for load testing.

Sources

  1. Stripe Docs — Testing
  2. Stripe Docs — Authenticate with 3D Secure
  3. Stripe Docs — Receive Stripe events in your webhook endpoint
  4. Stripe API Reference — Idempotent requests
  5. Stripe Docs — Go-live checklist

Tools used in this guide

Related guides

Written for developers, QA teams and merchants. Generated card numbers are test data: they are not real cards and cannot pay for anything.

Advertisement