/stripe-guides

How to fix Stripe payment failed issue?

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.

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 Stripe payment failed issue?

How to Fix Stripe Payment Failed Issue

 

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:

  • Insufficient funds in the customer's account
  • Card expired or invalid
  • Incorrect payment details
  • Bank declined the transaction
  • API or implementation errors in your code
  • Fraud detection mechanisms triggered

 

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:

  • In development: Use Stripe test keys (starting with 'sk_test_' and 'pk_test_')
  • In production: Use Stripe live keys (starting with 'sk_live_' and 'pk_live_')

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 (
    
{error &&
{error}
} ); };

 

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 (
    
setBillingDetails({...billingDetails, name: e.target.value})} required />
setBillingDetails({...billingDetails, email: e.target.value})} required />
setBillingDetails({ ...billingDetails, address: {...billingDetails.address, postal\_code: e.target.value} })} required />
{ setError(e.error ? e.error.message : ''); setCardComplete(e.complete); }} />
{error &&
{error}
} {succeeded ? (
Payment successful! Thank you for your purchase.
) : ( )}
); }; 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:

)}
{paymentMethods.map(method => (
setSelectedMethod(method.id)} />
))}
{selectedMethod === 'card' && } {selectedMethod !== 'card' && ( )}
); }; // 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.

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