Learn how to fix Stripe payment failed issues with step-by-step troubleshooting, error handling, API key checks, retry logic, 3D Secure, webhooks, and best practices.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Step 1: Identify the Cause of the Failure
Before implementing any fix, you need to understand why the payment failed. Stripe provides detailed error messages that can help identify the issue. Common reasons for payment failures include:
Step 2: Check the Stripe Error Response
When a payment fails, Stripe returns an error object that contains valuable information. Here's how to check the error response:
try {
// Attempt to create a payment
const paymentIntent = await stripe.paymentIntents.create({
amount: 2000,
currency: 'usd',
payment\_method: paymentMethodId,
confirm: true,
});
// Handle successful payment
return paymentIntent;
} catch (error) {
// Log the error for debugging
console.error('Payment failed:', error);
// Extract the error message
const errorMessage = error.message || 'Payment failed';
const errorType = error.type || 'unknown';
const errorCode = error.code || 'unknown';
// Now you can handle the error appropriately
return {
error: {
message: errorMessage,
type: errorType,
code: errorCode
}
};
}
Step 3: Implement Proper Error Handling in Your Frontend
Add robust error handling in your frontend to display meaningful error messages to your users:
// React example
const handlePaymentSubmit = async (event) => {
event.preventDefault();
setIsProcessing(true);
try {
const { error, paymentIntent } = await submitPayment();
if (error) {
setErrorMessage(error.message);
// Show user-friendly error message
if (error.code === 'card\_declined') {
setErrorMessage('Your card was declined. Please try another payment method.');
} else if (error.code === 'expired\_card') {
setErrorMessage('Your card has expired. Please update your card details.');
} else if (error.code === 'insufficient\_funds') {
setErrorMessage('Your card has insufficient funds. Please try another card.');
} else {
setErrorMessage('Payment failed. Please try again later.');
}
} else {
// Payment succeeded
setPaymentSuccess(true);
}
} catch (err) {
setErrorMessage('An unexpected error occurred. Please try again.');
console.error(err);
} finally {
setIsProcessing(false);
}
};
Step 4: Implement Proper Server-Side Error Handling
For Node.js/Express backend, implement proper error handling:
// Express route handler
app.post('/create-payment', async (req, res) => {
const { amount, currency, payment\_method } = req.body;
try {
// Create a PaymentIntent
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
payment\_method,
confirm: true,
return\_url: 'https://yourwebsite.com/success',
});
// Return the client secret to the client
res.json({
clientSecret: paymentIntent.client\_secret,
status: paymentIntent.status
});
} catch (error) {
// Handle different types of Stripe errors
let errorMessage = 'An error occurred with your payment';
let statusCode = 400;
if (error.type === 'StripeCardError') {
// Card was declined
errorMessage = error.message;
} else if (error.type === 'StripeInvalidRequestError') {
// Invalid parameters were supplied to Stripe's API
errorMessage = 'Invalid payment information provided';
statusCode = 400;
} else if (error.type === 'StripeAPIError') {
// Stripe's API experienced an error
errorMessage = 'Payment system error. Please try again later';
statusCode = 500;
} else if (error.type === 'StripeConnectionError') {
// Network communication with Stripe failed
errorMessage = 'Network error. Please try again later';
statusCode = 503;
} else if (error.type === 'StripeAuthenticationError') {
// Authentication with Stripe's API failed
errorMessage = 'Authentication with payment system failed';
statusCode = 500;
console.error('Stripe API key may be invalid:', error);
}
res.status(statusCode).json({ error: errorMessage });
}
});
Step 5: Check Your Stripe API Keys and Webhook Configuration
Make sure you're using the correct API keys in the right environment:
Here's how to properly configure your API keys:
// Node.js example
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Make sure to set the environment variables in your .env file
// STRIPE_SECRET_KEY=sk_test_your_test_key (for development)
// STRIPE_SECRET_KEY=sk_live_your_live_key (for production)
Step 6: Implement Retry Logic for Failed Payments
For transient errors, implement a retry mechanism:
const MAX\_RETRIES = 3;
const RETRY_DELAY_MS = 1000;
async function processPaymentWithRetry(paymentData, retryCount = 0) {
try {
const paymentIntent = await stripe.paymentIntents.create(paymentData);
return paymentIntent;
} catch (error) {
// Only retry for certain error types that might be temporary
const isRetryableError =
error.type === 'StripeConnectionError' ||
error.type === 'StripeAPIError' ||
(error.type === 'StripeCardError' && error.code === 'processing\_error');
if (isRetryableError && retryCount < MAX\_RETRIES) {
console.log(`Retrying payment after error: ${error.message}. Attempt ${retryCount + 1}`);
// Wait for a delay before retrying
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
// Retry the payment
return processPaymentWithRetry(paymentData, retryCount + 1);
}
// If we've exhausted retries or it's not a retryable error, throw the error
throw error;
}
}
Step 7: Implement Payment Confirmation and 3D Secure Authentication
For cards that require 3D Secure authentication, implement proper handling:
// Client-side code (React example with Stripe.js)
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
const CheckoutForm = () => {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setProcessing(true);
if (!stripe || !elements) {
// Stripe.js hasn't loaded yet
return;
}
// Create payment method
const cardElement = elements.getElement(CardElement);
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
setError(error.message);
setProcessing(false);
return;
}
// Send paymentMethod.id to your server
const response = await fetch('/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 1999,
payment_method_id: paymentMethod.id
})
});
const paymentData = await response.json();
// Handle 3D Secure authentication if required
if (paymentData.status === 'requires\_action') {
const { error, paymentIntent } = await stripe.confirmCardPayment(
paymentData.client\_secret
);
if (error) {
setError(`Payment failed: ${error.message}`);
} else if (paymentIntent.status === 'succeeded') {
// Payment succeeded after 3D Secure authentication
handlePaymentSuccess(paymentIntent);
}
} else if (paymentData.status === 'succeeded') {
// Payment succeeded without 3D Secure
handlePaymentSuccess(paymentData);
} else {
setError('Payment failed. Please try again.');
}
setProcessing(false);
};
return (
);
};
Step 8: Set Up Proper Webhook Handling
Set up webhooks to handle asynchronous payment events:
// Node.js/Express webhook handler
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
// This is your Stripe CLI webhook secret for testing
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
console.error(`Webhook Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'payment\_intent.succeeded':
const paymentIntent = event.data.object;
console.log(`PaymentIntent for ${paymentIntent.amount} was successful!`);
// Update your database, fulfill order, send email receipt, etc.
await updateOrderStatus(paymentIntent.metadata.orderId, 'paid');
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object;
console.log(`Payment failed: ${failedPayment.last_payment_error?.message}`);
// Notify customer of failed payment
await notifyCustomerOfFailedPayment(
failedPayment.metadata.orderId,
failedPayment.last_payment_error?.message
);
break;
default:
// Unexpected event type
console.log(`Unhandled event type ${event.type}`);
}
// Return a 200 response to acknowledge receipt of the event
res.send();
});
// Helper functions
async function updateOrderStatus(orderId, status) {
// Update order in your database
// Example: await db.collection('orders').updateOne({ id: orderId }, { $set: { status } });
}
async function notifyCustomerOfFailedPayment(orderId, errorMessage) {
// Notify customer via email or other means
// Example: await sendEmail(customer.email, 'Payment Failed', `Your payment for order ${orderId} failed: ${errorMessage}`);
}
Step 9: Log and Monitor Payment Failures
Implement logging to track payment failures:
// Logging helper function
async function logPaymentFailure(paymentIntent, error) {
const logEntry = {
paymentIntentId: paymentIntent.id,
amount: paymentIntent.amount,
currency: paymentIntent.currency,
customerId: paymentIntent.customer,
errorType: error.type,
errorCode: error.code,
errorMessage: error.message,
timestamp: new Date(),
metadata: paymentIntent.metadata
};
console.error('Payment failure:', logEntry);
// Save to your database for analysis
try {
// Example with MongoDB
await db.collection('payment\_failures').insertOne(logEntry);
} catch (dbError) {
console.error('Failed to log payment error to database:', dbError);
}
// You might also want to send alerts for critical errors
if (error.type === 'StripeAuthenticationError' ||
(error.type === 'StripeAPIError' && error.code === 'rate\_limit')) {
await sendAlertToTeam('Critical Stripe Error', logEntry);
}
}
// Function to send alerts to your team
async function sendAlertToTeam(subject, data) {
// Implementation depends on your alert system
// Could be email, Slack notification, etc.
// Example with email
await sendEmail(
'[email protected]',
`ALERT: ${subject}`,
`Payment system alert:\n${JSON.stringify(data, null, 2)}`
);
}
Step 10: Update Your Payment Form with Best Practices
Implement an optimized payment form that reduces errors:
// HTML/React payment form with validation and error handling
import React, { useState } from 'react';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
const CARD_ELEMENT_OPTIONS = {
style: {
base: {
color: '#32325d',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: 'antialiased',
fontSize: '16px',
'::placeholder': {
color: '#aab7c4'
}
},
invalid: {
color: '#fa755a',
iconColor: '#fa755a'
}
},
hidePostalCode: true // Handle postal code separately for better validation
};
const CheckoutForm = () => {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState(null);
const [cardComplete, setCardComplete] = useState(false);
const [processing, setProcessing] = useState(false);
const [succeeded, setSucceeded] = useState(false);
const [billingDetails, setBillingDetails] = useState({
name: '',
email: '',
phone: '',
address: {
line1: '',
city: '',
state: '',
postal\_code: '',
country: ''
}
});
// Validate form fields
const validateForm = () => {
if (!billingDetails.name) return "Name is required";
if (!billingDetails.email) return "Email is required";
if (!billingDetails.email.match(/^[^\s@]+@[^\s@]+.[^\s@]+$/)) return "Email is invalid";
if (!billingDetails.address.postal\_code) return "Postal code is required";
if (!cardComplete) return "Please complete your card details";
return null;
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) {
// Stripe.js hasn't loaded yet
return;
}
// Validate form
const validationError = validateForm();
if (validationError) {
setError(validationError);
return;
}
setProcessing(true);
// Create payment method with billing details
const cardElement = elements.getElement(CardElement);
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing\_details: billingDetails
});
if (error) {
setError(error.message);
setProcessing(false);
return;
}
// Send to your server
try {
const { error: backendError, clientSecret } = await fetch('/create-payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
paymentMethodId: paymentMethod.id,
amount: 1999, // $19.99
currency: 'usd',
receipt\_email: billingDetails.email,
metadata: {
customer\_name: billingDetails.name,
customer\_email: billingDetails.email
}
})
}).then(r => r.json());
if (backendError) {
setError(backendError.message);
setProcessing(false);
return;
}
// Handle 3D Secure authentication if needed
const { error: confirmError, paymentIntent } = await stripe.confirmCardPayment(
clientSecret
);
if (confirmError) {
setError(confirmError.message);
} else if (paymentIntent.status === 'succeeded') {
setSucceeded(true);
setError(null);
// You can redirect to a success page or show a success message
}
} catch (err) {
setError('An unexpected error occurred. Please try again later.');
console.error(err);
}
setProcessing(false);
};
return (
);
};
export default CheckoutForm;
Step 11: Implement Fallback Payment Methods
Provide alternative payment methods when a card fails:
// React component for fallback payment methods
const PaymentFallback = ({ initialError, amount, onPaymentSuccess }) => {
const [selectedMethod, setSelectedMethod] = useState('card'); // Default to card
const paymentMethods = [
{ id: 'card', name: 'Credit/Debit Card' },
{ id: 'paypal', name: 'PayPal' },
{ id: 'bank\_transfer', name: 'Bank Transfer' }
];
const handleSubmit = async (event) => {
event.preventDefault();
switch (selectedMethod) {
case 'card':
// Handle card payment (use the CheckoutForm component from previous step)
break;
case 'paypal':
// Redirect to PayPal
window.location.href = '/process-paypal-payment?amount=' + amount;
break;
case 'bank\_transfer':
// Show bank transfer instructions
try {
const response = await fetch('/generate-bank-transfer-instructions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount })
});
const data = await response.json();
// Display the bank transfer instructions to the user
showBankTransferInstructions(data);
} catch (error) {
console.error('Error generating bank transfer instructions:', error);
}
break;
}
};
return (
{initialError && (
Your payment could not be processed: {initialError}
Please try again or select an alternative payment method below:
)}
);
};
// Helper function to display bank transfer instructions
function showBankTransferInstructions(data) {
// Create a modal or panel to show the instructions
const modal = document.createElement('div');
modal.className = 'bank-transfer-modal';
modal.innerHTML = \`
\`;
document.body.appendChild(modal);
// Add event listener to close button
modal.querySelector('.close-button').addEventListener('click', () => {
document.body.removeChild(modal);
});
}
Step 12: Test and Debug Your Payment Flow
Use Stripe's test cards to simulate different scenarios:
// Test different payment scenarios
// 1. Successful payment
// Card number: 4242 4242 4242 4242
// Expiry: Any future date
// CVC: Any 3 digits
// ZIP: Any 5 digits
// 2. Card declined
// Card number: 4000 0000 0000 0002
// Expiry: Any future date
// CVC: Any 3 digits
// ZIP: Any 5 digits
// 3. Card requiring authentication (3D Secure)
// Card number: 4000 0000 0000 3220
// Expiry: Any future date
// CVC: Any 3 digits
// ZIP: Any 5 digits
// 4. Insufficient funds error
// Card number: 4000 0000 0000 9995
// Expiry: Any future date
// CVC: Any 3 digits
// ZIP: Any 5 digits
// 5. Processing error
// Card number: 4000 0000 0000 0119
// Expiry: Any future date
// CVC: Any 3 digits
// ZIP: Any 5 digits
// Test helper function
async function testPaymentScenarios() {
const testCases = [
{
name: 'Successful payment',
card: '4242424242424242',
expectSuccess: true
},
{
name: 'Card declined',
card: '4000000000000002',
expectSuccess: false,
expectedError: 'Your card was declined.'
},
{
name: '3D Secure required',
card: '4000000000003220',
expectSuccess: true,
requires3DS: true
},
{
name: 'Insufficient funds',
card: '4000000000009995',
expectSuccess: false,
expectedError: 'Your card has insufficient funds.'
},
{
name: 'Processing error',
card: '4000000000000119',
expectSuccess: false,
expectedError: 'An error occurred while processing your card.'
}
];
console.log('Starting payment scenario tests...');
for (const testCase of testCases) {
console.log(`Testing scenario: ${testCase.name}`);
try {
// This is a mock function - in reality you would use the Stripe API
const result = await processTestPayment({
card: testCase.card,
exp\_month: 12,
exp\_year: 2030,
cvc: '123',
amount: 2000,
currency: 'usd'
});
if (testCase.expectSuccess) {
console.log(`✅ Test passed: Payment succeeded as expected`);
} else {
console.log(`❌ Test failed: Expected payment to fail but it succeeded`);
}
} catch (error) {
if (!testCase.expectSuccess) {
if (error.message.includes(testCase.expectedError)) {
console.log(`✅ Test passed: Payment failed with expected error: ${error.message}`);
} else {
console.log(`⚠️ Test partially passed: Payment failed but with unexpected error message. Expected: "${testCase.expectedError}", Got: "${error.message}"`);
}
} else {
console.log(`❌ Test failed: Expected payment to succeed but got error: ${error.message}`);
}
}
console.log('-------------------');
}
console.log('Payment scenario tests completed.');
}
// Mock function to process test payments - in reality, you would use the Stripe API
async function processTestPayment(paymentData) {
// Simulate API call to Stripe
return new Promise((resolve, reject) => {
setTimeout(() => {
// Simulate different responses based on the card number
switch (paymentData.card) {
case '4242424242424242':
resolve({ id: 'pi_test_success', status: 'succeeded' });
break;
case '4000000000003220':
resolve({
id: 'pi_test_3ds',
status: 'requires\_action',
next\_action: {
type: 'use_stripe_sdk',
use_stripe_sdk: {
type: 'three_d_secure\_redirect',
stripe_js: 'https://hooks.stripe.com/3d_secure_2_eap/begin_test/src_1Iqx3oBNJ02ErVOjmjf7QDLt/src_client_secret\_ELiuqTuimw8W7tw8DMhV4FRc'
}
}
});
break;
case '4000000000000002':
reject(new Error('Your card was declined.'));
break;
case '4000000000009995':
reject(new Error('Your card has insufficient funds.'));
break;
case '4000000000000119':
reject(new Error('An error occurred while processing your card.'));
break;
default:
reject(new Error('Invalid card information'));
}
}, 1000); // Simulate network delay
});
}
By following these steps, you should be able to identify and fix most Stripe payment failure issues. Remember that payment processing is complex, and a robust implementation requires thorough error handling, proper logging, and a good user experience that guides customers through any payment issues.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.