/stripe-guides

How to test failed payments in Stripe?

Learn how to test failed payments in Stripe using test cards, API, Stripe Elements, webhooks, and the Stripe CLI to ensure robust error handling in your application.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to test failed payments in Stripe?

How to Test Failed Payments in Stripe

 

In this comprehensive tutorial, I'll walk you through various methods to test failed payments in Stripe. Testing payment failures is crucial for ensuring your application handles errors gracefully and provides appropriate feedback to users.

 

Step 1: Set Up Your Stripe Testing Environment

 

Before testing failed payments, ensure you're in Stripe's test mode:

// Make sure your API keys are set to test mode
const stripe = require('stripe')('sk_test_YourTestSecretKey');

Always use test keys (prefixed with sk_test_) rather than live keys (sk_live_) to avoid processing real payments during testing.

 

Step 2: Use Stripe's Test Card Numbers

 

Stripe provides specific test card numbers that trigger different error scenarios:

// Generic declined card
const declinedCard = '4000000000000002';

// Declined for insufficient funds
const insufficientFundsCard = '4000000000009995';

// Declined for lost card
const lostCard = '4000000000009987';

// Declined for stolen card
const stolenCard = '4000000000009979';

// Declined for expired card
const expiredCard = '4000000000000069';

// Declined for incorrect CVC
const incorrectCvcCard = '4000000000000127';

// Declined for processing error
const processingErrorCard = '4000000000000119';

Use these card numbers with any valid expiration date in the future, any 3-digit CVC, and any postal code.

 

Step 3: Testing Card Decline Using the Stripe API

 

Here's how to test a declined payment using the Stripe API:

async function testDeclinedPayment() {
  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: 2000, // $20.00
      currency: 'usd',
      payment_method_data: {
        type: 'card',
        card: {
          number: '4000000000000002', // Declined card
          exp\_month: 12,
          exp\_year: 2024,
          cvc: '123',
        },
      },
      confirm: true, // Confirm the payment immediately
    });
    
    console.log('Payment status:', paymentIntent.status);
  } catch (error) {
    console.error('Payment failed:', error.message);
    console.log('Decline code:', error.decline\_code);
    console.log('Error type:', error.type);
  }
}

testDeclinedPayment();

 

Step 4: Testing Failed Payments with Stripe Elements in Frontend

 

To test failed payments in your frontend application using Stripe Elements:

// HTML setup
// JavaScript implementation const stripe = Stripe('pk_test_YourTestPublishableKey'); const elements = stripe.elements(); const cardElement = elements.create('card'); cardElement.mount('#card-element'); const form = document.getElementById('payment-form'); const errorDisplay = document.getElementById('error-message'); form.addEventListener('submit', async (event) => { event.preventDefault(); const { paymentMethod, error } = await stripe.createPaymentMethod({ type: 'card', card: cardElement, }); if (error) { // Handle error in card details errorDisplay.textContent = error.message; return; } // Send to your server const response = await fetch('/create-payment-intent', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ paymentMethodId: paymentMethod.id, amount: 2000, // $20.00 }), }); const result = await response.json(); if (result.error) { // Display error from server errorDisplay.textContent = result.error.message; } else { // Handle successful payment } });

 

Step 5: Server-Side Implementation for Handling Failed Payments

 

Implement proper error handling on your server:

// Node.js with Express example
const express = require('express');
const app = express();
const stripe = require('stripe')('sk_test_YourTestSecretKey');

app.use(express.json());

app.post('/create-payment-intent', async (req, res) => {
  try {
    const { paymentMethodId, amount } = req.body;
    
    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency: 'usd',
      payment\_method: paymentMethodId,
      confirm: true,
      error_on_requires\_action: true, // Decline the payment if authentication is required
    });
    
    res.json({ success: true, paymentIntent });
  } catch (error) {
    console.log('Payment failed:', error);
    
    let errorMessage;
    switch (error.code) {
      case 'card\_declined':
        errorMessage = `Payment failed: ${error.decline_code}`;
        break;
      case 'expired\_card':
        errorMessage = 'Your card has expired.';
        break;
      case 'incorrect\_cvc':
        errorMessage = 'The CVC number is incorrect.';
        break;
      case 'processing\_error':
        errorMessage = 'An error occurred while processing your card.';
        break;
      default:
        errorMessage = 'An unexpected error occurred.';
    }
    
    res.status(400).json({
      error: {
        message: errorMessage,
        type: error.type,
        code: error.code,
        decline_code: error.decline_code
      }
    });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

 

Step 6: Testing Authentication Failures

 

To test 3D Secure authentication failures:

// 3D Secure authentication required card
const authRequiredCard = '4000000000003220';

// 3D Secure authentication failed card
const authFailedCard = '4000008400001629';

async function test3DSecureFailure() {
  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: 1999,
      currency: 'usd',
      payment_method_data: {
        type: 'card',
        card: {
          number: authFailedCard,
          exp\_month: 12,
          exp\_year: 2024,
          cvc: '123',
        },
      },
      confirm: true,
      return\_url: 'https://your-website.com/return', // For redirect flow
    });
    
    console.log('Payment Intent status:', paymentIntent.status);
    console.log('Next action:', paymentIntent.next\_action);
  } catch (error) {
    console.error('Authentication failed:', error.message);
  }
}

test3DSecureFailure();

 

Step 7: Testing Rate Limiting and Network Errors

 

To simulate network errors and rate limiting:

// Test rate limiting by making multiple rapid requests
async function testRateLimiting() {
  try {
    // Make many requests in quick succession
    const promises = [];
    for (let i = 0; i < 100; i++) {
      promises.push(stripe.customers.create({
        email: `test${i}@example.com`,
      }));
    }
    
    await Promise.all(promises);
  } catch (error) {
    if (error.type === 'rate_limit_error') {
      console.log('Rate limit exceeded:', error.message);
    } else {
      console.error('Unexpected error:', error);
    }
  }
}

// Simulate network timeout
async function simulateNetworkTimeout() {
  // Override Stripe's default timeout (mock implementation)
  const originalFetch = global.fetch;
  global.fetch = () => new Promise((resolve) => {
    // Never resolve to simulate timeout
    setTimeout(() => {
      // Restore original fetch after test
      global.fetch = originalFetch;
    }, 60000);
  });
  
  try {
    await stripe.customers.create({ email: '[email protected]' });
  } catch (error) {
    console.log('Network timeout error:', error);
    // Restore fetch in case of error
    global.fetch = originalFetch;
  }
}

 

Step 8: Testing Webhook Failure Scenarios

 

Test how your application handles webhook failures:

// Mock failed webhook event for payment_intent.payment_failed
const mockFailedPaymentEvent = {
  id: 'evt_test_webhook_payment_failed',
  object: 'event',
  type: 'payment_intent.payment_failed',
  data: {
    object: {
      id: 'pi\_test123456',
      object: 'payment\_intent',
      amount: 2000,
      currency: 'usd',
      status: 'requires_payment_method',
      last_payment_error: {
        code: 'card\_declined',
        decline_code: 'insufficient_funds',
        message: 'Your card has insufficient funds.'
      }
    }
  }
};

// Express route to handle webhook
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  
  // For testing, use mock event instead of parsing the request
  event = mockFailedPaymentEvent;
  
  /\* In production, you would verify the webhook:
  
  const signature = req.headers['stripe-signature'];
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      signature,
      'whsec_your_webhook\_secret'
    );
  } catch (err) {
    console.log('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  \*/
  
  if (event.type === 'payment_intent.payment_failed') {
    const paymentIntent = event.data.object;
    console.log('Payment failed:', paymentIntent.id);
    console.log('Failure reason:', paymentIntent.last_payment_error);
    
    // Here you would update your database or notify the customer
    // For example:
    // await updateOrderStatus(paymentIntent.metadata.orderId, 'payment\_failed');
    // await sendPaymentFailureEmail(paymentIntent.metadata.customerEmail);
  }
  
  res.status(200).json({ received: true });
});

 

Step 9: Testing Specific Error Scenarios with the Stripe CLI

 

The Stripe CLI is a powerful tool for testing webhooks and simulating events:

  1. Install the Stripe CLI from https://stripe.com/docs/stripe-cli
  2. Login with your Stripe account:
stripe login
  1. Trigger test events:
stripe trigger payment_intent.payment_failed
  1. Forward webhooks to your local environment:
stripe listen --forward-to http://localhost:3000/webhook

 

Step 10: Comprehensive End-to-End Testing

 

Create a comprehensive test suite that covers various payment failure scenarios:

// Using Jest as the testing framework
const stripe = require('stripe')('sk_test_YourTestSecretKey');

describe('Stripe Payment Failure Scenarios', () => {
  test('should handle card declined for insufficient funds', async () => {
    expect.assertions(1);
    
    try {
      await stripe.paymentIntents.create({
        amount: 2000,
        currency: 'usd',
        payment_method_data: {
          type: 'card',
          card: {
            number: '4000000000009995', // Insufficient funds card
            exp\_month: 12,
            exp\_year: 2024,
            cvc: '123',
          },
        },
        confirm: true,
      });
    } catch (error) {
      expect(error.decline_code).toBe('insufficient_funds');
    }
  });
  
  test('should handle expired card', async () => {
    expect.assertions(1);
    
    try {
      await stripe.paymentIntents.create({
        amount: 2000,
        currency: 'usd',
        payment_method_data: {
          type: 'card',
          card: {
            number: '4000000000000069', // Expired card
            exp\_month: 12,
            exp\_year: 2024,
            cvc: '123',
          },
        },
        confirm: true,
      });
    } catch (error) {
      expect(error.code).toBe('expired\_card');
    }
  });
  
  test('should handle incorrect CVC', async () => {
    expect.assertions(1);
    
    try {
      await stripe.paymentIntents.create({
        amount: 2000,
        currency: 'usd',
        payment_method_data: {
          type: 'card',
          card: {
            number: '4000000000000127', // Incorrect CVC card
            exp\_month: 12,
            exp\_year: 2024,
            cvc: '123',
          },
        },
        confirm: true,
      });
    } catch (error) {
      expect(error.code).toBe('incorrect\_cvc');
    }
  });
  
  // Add more test cases for other scenarios
});

 

Conclusion

 

Testing failed payments in Stripe is essential for building a robust payment system. By using the test card numbers and strategies outlined in this guide, you can ensure your application handles payment failures gracefully, providing a better user experience.

Remember to always use test API keys when testing and to implement proper error handling on both client and server sides. With comprehensive testing, you can confidently deploy your payment integration knowing it will handle real-world payment issues appropriately.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022