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.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
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:
stripe login
stripe trigger payment_intent.payment_failed
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.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.