/stripe-guides

How to fix “card declined” error in Stripe API?

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.

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 fix “card declined” error in Stripe API?

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:

  • card\_declined: General decline by the card issuer
  • insufficient\_funds: The card doesn't have enough funds
  • lost\_card: The card has been reported lost
  • stolen\_card: The card has been reported stolen
  • expired\_card: The card has expired
  • incorrect\_cvc: The CVC number is incorrect
  • processing\_error: An error occurred while processing the card
  • incorrect\_number: The card number is incorrect

 

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:

  1. Navigate to the Radar section in your Stripe Dashboard
  2. Create custom rules to handle specific decline scenarios

// 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.

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