Learn how to fix “card declined” errors in Stripe API with this step-by-step guide covering error codes, troubleshooting, secure payments, retries, 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.
How to Fix "Card Declined" Error in Stripe API: A Comprehensive Tutorial
Introduction
When integrating Stripe for payment processing, you might encounter the dreaded "card declined" error. This error occurs when a payment attempt fails due to various reasons related to the card itself or the way the payment is being processed. In this detailed tutorial, I'll walk you through the common causes and solutions for fixing card declined errors in the Stripe API.
Step 1: Understand the Stripe Error Codes
Before diving into solutions, it's essential to understand what Stripe is telling you through its error codes. When a card is declined, Stripe returns specific error codes that help identify the issue:
Step 2: Check Your Stripe Dashboard for Error Details
Navigate to your Stripe Dashboard and look for the failed payment in the "Payments" section. Click on the failed payment to see detailed information about why the card was declined.
// Example URL for Stripe Dashboard Payments section
https://dashboard.stripe.com/payments
Step 3: Implement Proper Error Handling in Your Code
Ensure your code properly catches and handles Stripe errors. Here's an example in Node.js:
const stripe = require('stripe')('sk_test_your_secret_key');
async function createCharge(amount, cardToken, description) {
try {
const charge = await stripe.charges.create({
amount: amount,
currency: 'usd',
source: cardToken,
description: description
});
return { success: true, charge: charge };
} catch (error) {
console.error('Error:', error.message);
// Handle specific card errors
if (error.type === 'StripeCardError') {
const errorMessage = getCustomerFriendlyMessage(error.code);
return { success: false, error: errorMessage, code: error.code };
}
return { success: false, error: 'An unexpected error occurred' };
}
}
function getCustomerFriendlyMessage(errorCode) {
switch(errorCode) {
case 'card\_declined':
return 'Your card was declined. Please try a different payment method.';
case 'insufficient\_funds':
return 'Your card has insufficient funds. Please try a different card.';
case 'expired\_card':
return 'Your card has expired. Please try a different card.';
case 'incorrect\_cvc':
return 'The security code you entered is invalid. Please check and try again.';
default:
return 'There was an issue processing your payment. Please try again.';
}
}
Step 4: Use Stripe Elements for Secure Card Collection
Implement Stripe Elements in your frontend to securely collect card information and reduce errors:
Stripe Elements Example
Step 5: Implement 3D Secure Authentication
For European cards and to comply with Strong Customer Authentication (SCA), implement 3D Secure:
const stripe = require('stripe')('sk_test_your_secret_key');
async function createPaymentIntent(amount, customer) {
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: 'usd',
customer: customer,
payment_method_types: ['card'],
// Enable 3D Secure by default
setup_future_usage: 'off\_session',
// Optionally set a return URL after 3D Secure
confirm: true,
return\_url: 'https://yourwebsite.com/payment-complete'
});
return { success: true, client_secret: paymentIntent.client_secret };
} catch (error) {
console.error('Error creating PaymentIntent:', error);
return { success: false, error: error.message };
}
}
// On the client side, handle the 3D Secure authentication
// Frontend code to handle the 3D Secure challenge
// Client-side code for handling 3D Secure
const stripe = Stripe('pk_test_your_publishable_key');
async function handlePayment(paymentIntentClientSecret) {
const result = await stripe.confirmCardPayment(paymentIntentClientSecret, {
payment\_method: {
card: elements.getElement('card'),
billing\_details: {
name: 'Customer Name',
},
},
});
if (result.error) {
// Show error to your customer
console.error(result.error.message);
} else {
if (result.paymentIntent.status === 'succeeded') {
// The payment succeeded!
console.log('Payment successful!');
} else if (result.paymentIntent.status === 'requires\_action') {
// The card requires 3D Secure authentication
const { error, paymentIntent } = await stripe.confirmCardPayment(
paymentIntentClientSecret
);
if (error) {
// Payment failed
console.error('Payment failed:', error);
} else if (paymentIntent.status === 'succeeded') {
// Payment succeeded after 3D Secure authentication
console.log('Payment successful after 3D Secure!');
}
}
}
}
Step 6: Use Stripe's Test Cards
During development, use Stripe's test cards to simulate various scenarios:
// Always successful card
const testCardSuccess = {
number: '4242 4242 4242 4242',
exp\_month: 12,
exp\_year: 2024,
cvc: '123'
};
// Card that will be declined
const testCardDeclined = {
number: '4000 0000 0000 0002',
exp\_month: 12,
exp\_year: 2024,
cvc: '123'
};
// Card that requires 3D Secure authentication
const testCard3DSecure = {
number: '4000 0000 0000 3220',
exp\_month: 12,
exp\_year: 2024,
cvc: '123'
};
// Card that will be declined for insufficient funds
const testCardInsufficientFunds = {
number: '4000 0000 0000 9995',
exp\_month: 12,
exp\_year: 2024,
cvc: '123'
};
Step 7: Use Radar Rules to Reduce Declines
Configure Stripe Radar rules to reduce false declines:
// Example of a Radar rule (this is set up in the Stripe Dashboard, not in code)
// "If the payment amount is less than $10 and the card is declined for insufficient\_funds,
// allow the payment to go through anyway"
Step 8: Implement Retry Logic for Failed Payments
Set up an automated retry system for declined transactions:
const stripe = require('stripe')('sk_test_your_secret_key');
async function retryFailedPayment(paymentIntentId, maxRetries = 3, delay = 24 _ 60 _ 60 \* 1000) { // Default 24-hour delay
// Get the failed payment intent
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
// Check if it's in a state that can be retried
if (paymentIntent.status === 'requires_payment_method') {
// Store retry information in metadata
const currentRetry = paymentIntent.metadata.retry\_count ?
parseInt(paymentIntent.metadata.retry\_count) : 0;
if (currentRetry < maxRetries) {
// Update metadata
await stripe.paymentIntents.update(paymentIntentId, {
metadata: {
retry\_count: currentRetry + 1,
last_retry_date: new Date().toISOString()
}
});
// Schedule retry (using a job queue in a production environment)
setTimeout(async () => {
try {
// Attempt to confirm the payment again
const result = await stripe.paymentIntents.confirm(paymentIntentId);
console.log(`Retry ${currentRetry + 1} result:`, result.status);
} catch (error) {
console.error(`Retry ${currentRetry + 1} failed:`, error.message);
}
}, delay);
return {
success: true,
message: `Scheduled retry ${currentRetry + 1} of ${maxRetries}`
};
} else {
return {
success: false,
message: 'Maximum retry attempts reached'
};
}
} else {
return {
success: false,
message: `Payment is in '${paymentIntent.status}' state, cannot retry`
};
}
}
Step 9: Implement Card Verification Before Charging
Verify cards before attempting to charge them:
const stripe = require('stripe')('sk_test_your_secret_key');
async function verifyCardBeforeCharge(cardToken) {
try {
// Create a $0 or $1 authorization to verify the card
const charge = await stripe.charges.create({
amount: 100, // $1.00
currency: 'usd',
source: cardToken,
capture: false // This creates an authorization only, doesn't capture the funds
});
// If successful, immediately void the authorization
await stripe.refunds.create({
charge: charge.id
});
return { verified: true };
} catch (error) {
console.error('Card verification failed:', error.message);
return {
verified: false,
reason: error.code || error.type,
message: error.message
};
}
}
Step 10: Implement Webhooks to Monitor Payment Status
Set up webhooks to monitor payment statuses and respond to changes:
// Server code (Node.js with Express)
const express = require('express');
const stripe = require('stripe')('sk_test_your_secret_key');
const app = express();
// Use JSON parser for webhooks
app.post('/webhook', express.raw({type: 'application/json'}), (request, response) => {
const sig = request.headers['stripe-signature'];
const endpointSecret = 'whsec_your_webhook_signing_secret';
let event;
try {
event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
} catch (err) {
response.status(400).send(`Webhook Error: ${err.message}`);
return;
}
// Handle specific events
switch (event.type) {
case 'charge.succeeded':
const chargeSucceeded = event.data.object;
// Handle successful charge
console.log('Charge succeeded:', chargeSucceeded.id);
break;
case 'charge.failed':
const chargeFailed = event.data.object;
console.log('Charge failed:', chargeFailed.id);
console.log('Failure reason:', chargeFailed.failure\_message);
// Log details for analysis
logFailedCharge(chargeFailed);
// Optionally trigger retry flow
if (chargeFailed.failure_code === 'card_declined') {
// Notify customer and suggest retry
notifyCustomerAboutFailedCharge(chargeFailed);
}
break;
// More event types...
}
response.send();
});
function logFailedCharge(charge) {
// Implementation to log details to your database
console.log(\`
Charge ID: ${charge.id}
Amount: ${charge.amount}
Currency: ${charge.currency}
Customer: ${charge.customer}
Failure Code: ${charge.failure\_code}
Failure Message: ${charge.failure\_message}
Time: ${new Date().toISOString()}
\`);
}
function notifyCustomerAboutFailedCharge(charge) {
// Implementation to send email/SMS to customer
console.log(`Notifying customer ${charge.customer} about failed charge ${charge.id}`);
}
app.listen(3000, () => console.log('Webhook server running on port 3000'));
Step 11: Implement Address Verification System (AVS)
Enhance security with AVS to reduce declines due to fraud concerns:
const stripe = require('stripe')('sk_test_your_secret_key');
async function createCardWithAVS(customerId, cardToken, billingDetails) {
try {
const card = await stripe.customers.createSource(
customerId,
{
source: cardToken,
}
);
// Update the card with billing details for AVS
const updatedCard = await stripe.customers.updateSource(
customerId,
card.id,
{
address_line1: billingDetails.address_line1,
address_line2: billingDetails.address_line2,
address\_city: billingDetails.city,
address\_state: billingDetails.state,
address_zip: billingDetails.postal_code,
address\_country: billingDetails.country,
}
);
return { success: true, card: updatedCard };
} catch (error) {
console.error('Error creating card with AVS:', error);
return { success: false, error: error.message };
}
}
Step 12: Analyze and Track Decline Rates
Implement a system to track and analyze decline rates:
// Database schema (SQL example)
CREATE TABLE payment\_attempts (
id SERIAL PRIMARY KEY,
customer\_id VARCHAR(255) NOT NULL,
payment_intent_id VARCHAR(255),
amount INTEGER NOT NULL,
currency VARCHAR(3) NOT NULL,
status VARCHAR(50) NOT NULL,
error\_code VARCHAR(100),
error\_message TEXT,
card\_country VARCHAR(2),
card\_brand VARCHAR(50),
card\_last4 VARCHAR(4),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
// Code to log payment attempts
async function logPaymentAttempt(paymentData) {
const query = \`
INSERT INTO payment\_attempts
(customer_id, payment_intent_id, amount, currency, status, error_code, error_message, card_country, card_brand, card_last4)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id;
\`;
const values = [
paymentData.customerId,
paymentData.paymentIntentId,
paymentData.amount,
paymentData.currency,
paymentData.status,
paymentData.errorCode || null,
paymentData.errorMessage || null,
paymentData.cardCountry || null,
paymentData.cardBrand || null,
paymentData.cardLast4 || null
];
try {
const result = await db.query(query, values);
return { success: true, id: result.rows[0].id };
} catch (error) {
console.error('Error logging payment attempt:', error);
return { success: false, error: error.message };
}
}
// Query to analyze decline rates
async function getDeclineAnalytics(startDate, endDate) {
const query = \`
SELECT
error\_code,
COUNT(\*) as decline\_count,
card\_brand,
card\_country,
TO_CHAR(DATE_TRUNC('day', created\_at), 'YYYY-MM-DD') as day
FROM
payment\_attempts
WHERE
status = 'failed'
AND created\_at BETWEEN $1 AND $2
GROUP BY
error_code, card_brand, card\_country, day
ORDER BY
decline\_count DESC;
\`;
try {
const result = await db.query(query, [startDate, endDate]);
return { success: true, data: result.rows };
} catch (error) {
console.error('Error getting decline analytics:', error);
return { success: false, error: error.message };
}
}
Conclusion
Handling card declined errors in Stripe requires a multi-faceted approach. By implementing proper error handling, using Stripe Elements, supporting 3D Secure authentication, and setting up retry mechanisms, you can significantly reduce the impact of declined payments on your business. Additionally, tracking and analyzing decline patterns will help you identify and address specific issues affecting your payment success rate.
Remember that different regions and card types may have unique requirements and decline patterns. Regularly reviewing your Stripe Dashboard and payment logs will help you stay on top of any emerging issues and maintain high payment acceptance rates.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.