# How to test failed payments in Stripe

- Tool: Stripe
- Difficulty: Beginner
- Time required: 15 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+, Stripe test mode
- Last updated: March 2026

## TL;DR

Test failed payments in Stripe using special test card numbers that simulate declines. Card 4000000000000002 triggers a generic decline, 4000000000009995 simulates insufficient funds, and 4000000000000069 triggers an expired card error. Always test failure paths in test mode before going live to ensure your app handles errors gracefully.

## Why Test Failed Payments?

Most developers only test the happy path — successful payments. But in production, 5-15% of payments fail due to insufficient funds, expired cards, fraud blocks, or network errors. If your app does not handle these failures gracefully, customers see confusing errors or get stuck on broken checkout pages. Stripe provides a set of test card numbers that simulate every type of decline so you can build proper error handling before going live.

## Before you start

- A Stripe account in test mode (toggle in the Dashboard)
- Node.js 18 or newer installed
- The stripe npm package installed (npm install stripe)
- Your test secret key (sk_test_...) from Dashboard → Developers → API keys

## Step-by-step guide

### 1. Use the generic decline test card

Card number 4000000000000002 simulates a generic card decline. Use it to test your basic error handling.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

try {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 2000, // $20.00
    currency: 'usd',
    payment_method: 'pm_card_chargeDeclined',
    confirm: true,
    automatic_payment_methods: {
      enabled: true,
      allow_redirects: 'never',
    },
  });
} catch (err) {
  console.log('Decline code:', err.code);
  console.log('Message:', err.message);
  // err.code === 'card_declined'
}
```

**Expected result:** The API throws a card_declined error with a 402 status code.

### 2. Test specific decline reasons

Use different test cards to simulate specific failure scenarios. Each card triggers a different decline_code that you can use to show the customer a helpful message.

```
// Insufficient funds
// Card: 4000000000009995
// pm token: pm_card_chargeDeclinedInsufficientFunds

// Expired card
// Card: 4000000000000069
// pm token: pm_card_chargeDeclinedExpiredCard

// Incorrect CVC
// Card: 4000000000000127
// pm token: pm_card_chargeDeclinedIncorrectCvc

// Processing error
// Card: 4000000000000119
// pm token: pm_card_chargeDeclinedProcessingError

// Lost card
// Card: 4000000000009987

// Stolen card
// Card: 4000000000009979
```

**Expected result:** Each test card produces a different decline_code (insufficient_funds, expired_card, incorrect_cvc, processing_error).

### 3. Handle decline errors in your code

Catch Stripe errors and map decline codes to user-friendly messages. Display these messages in your checkout UI instead of raw error text.

```
function getDeclineMessage(err) {
  if (err.type !== 'StripeCardError') {
    return 'An unexpected error occurred. Please try again.';
  }

  const messages = {
    card_declined: 'Your card was declined. Please try a different card.',
    insufficient_funds: 'Insufficient funds. Please try a different card or contact your bank.',
    expired_card: 'Your card has expired. Please use a different card.',
    incorrect_cvc: 'The CVC number is incorrect. Please check and try again.',
    processing_error: 'A processing error occurred. Please try again in a moment.',
    lost_card: 'This card has been reported lost. Please use a different card.',
    stolen_card: 'This card has been reported stolen. Please use a different card.',
  };

  return messages[err.decline_code] || messages.card_declined;
}
```

**Expected result:** Your app shows a clear, specific message for each type of decline.

### 4. Test 3D Secure failure

Card 4000008400001629 triggers a 3D Secure authentication that the customer fails. This tests your handling of requires_action status followed by authentication failure.

```
// Card: 4000008400001629 triggers 3DS that always fails
// Card: 4000000000003220 triggers 3DS that can succeed or fail

const paymentIntent = await stripe.paymentIntents.create({
  amount: 3000, // $30.00
  currency: 'usd',
  payment_method: 'pm_card_authenticationRequiredChargeDeclinedInsufficientFunds',
  confirm: true,
  return_url: 'https://yoursite.com/payment-result',
});

console.log('Status:', paymentIntent.status);
// 'requires_action' — customer must complete 3DS
```

**Expected result:** The PaymentIntent has status requires_action. After the customer fails 3DS authentication, the status changes to requires_payment_method.

## Complete code example

File: `test-failures.js`

```javascript
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();
app.use(express.json());

// Map decline codes to user-friendly messages
const DECLINE_MESSAGES = {
  card_declined: 'Your card was declined. Please try a different card.',
  insufficient_funds: 'Insufficient funds. Please try another payment method.',
  expired_card: 'Your card has expired. Please update your card details.',
  incorrect_cvc: 'Incorrect CVC. Please check the number on the back of your card.',
  processing_error: 'A processing error occurred. Please try again.',
  lost_card: 'This card cannot be used. Please try a different card.',
  stolen_card: 'This card cannot be used. Please try a different card.',
};

app.post('/api/pay', async (req, res) => {
  try {
    const { paymentMethodId, amount } = req.body;

    const paymentIntent = await stripe.paymentIntents.create({
      amount, // in cents
      currency: 'usd',
      payment_method: paymentMethodId,
      confirm: true,
      automatic_payment_methods: {
        enabled: true,
        allow_redirects: 'never',
      },
    });

    res.json({ status: paymentIntent.status, id: paymentIntent.id });
  } catch (err) {
    if (err.type === 'StripeCardError') {
      const message = DECLINE_MESSAGES[err.decline_code] || DECLINE_MESSAGES.card_declined;
      return res.status(402).json({
        error: message,
        decline_code: err.decline_code,
      });
    }
    res.status(500).json({ error: 'An unexpected error occurred.' });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server on port ${PORT}`));
```

## Common mistakes

- **Only testing with 4242424242424242 (the success card)** — undefined Fix: Test with decline cards too. In production, a significant percentage of charges fail. Your app must handle each decline type gracefully.
- **Showing raw Stripe error messages to customers** — undefined Fix: Map decline_code values to friendly, non-technical messages. Never expose internal error details or Stripe error IDs to end users.
- **Not testing 3D Secure failures** — undefined Fix: Use card 4000008400001629 to test 3DS authentication failure. Many European cards require 3DS, so this path must work.
- **Using test cards in live mode** — undefined Fix: Test card numbers only work with test API keys (sk_test_). They are rejected in live mode. Always verify you are in test mode.

## Best practices

- Test every decline code your app might encounter: insufficient funds, expired card, incorrect CVC, and processing errors
- Map each decline_code to a user-friendly message — never show raw API errors to customers
- Test 3D Secure flows with cards 4000000000003220 and 4000008400001629
- Verify that your frontend gracefully handles error responses and lets customers retry with a different card
- Test with different amounts — some test behaviors are amount-dependent
- Log decline codes and rates to monitor payment health in production
- Use Stripe's test clocks to simulate subscription payment failures over time
- Test the full flow end-to-end: decline → error message → retry → success

## Frequently asked questions

### Where can I find a full list of Stripe test card numbers?

Visit docs.stripe.com/testing. The page lists all test cards for declines, 3D Secure, different card brands, different countries, and specific error scenarios.

### Do test declines affect my account standing?

No. Test mode transactions are completely separate from live mode. Declines in test mode do not affect your account health, dispute rate, or any metrics.

### Can I trigger a specific error amount in test mode?

Some test behaviors are triggered by card number, not amount. However, for certain error simulations like partial captures, the amount matters. Check Stripe's testing documentation for amount-specific behaviors.

### How do I test webhook events for failed payments?

Use the Stripe CLI with 'stripe trigger payment_intent.payment_failed' to send test webhook events to your local server, or create actual test declines and let the webhook fire naturally.

### What if I need help building a robust payment error handling system?

For applications that need retry logic, smart routing, dunning flows, and comprehensive decline analytics, the RapidDev team can help architect a production-grade payment system.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-test-failed-payments-in-stripe
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-test-failed-payments-in-stripe
