/stripe-guides

How to retry failed invoice payments in Stripe?

Learn how to retry failed invoice payments in Stripe using the Dashboard, API, and webhooks. Improve recovery rates with smart retries and customer notifications.

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 retry failed invoice payments in Stripe?

How to Retry Failed Invoice Payments in Stripe

 

Introduction

 

Stripe is a popular payment processing platform that allows businesses to handle online payments. Sometimes, invoice payments fail due to various reasons such as insufficient funds, expired cards, or network issues. Fortunately, Stripe provides several ways to retry these failed payments. This tutorial will guide you through the different methods to retry failed invoice payments in Stripe.

 

Step 1: Understanding Why Payments Fail

 

Before attempting to retry payments, it's important to understand why they failed in the first place. Common reasons include:

  • Insufficient funds in the customer's account
  • Expired or canceled cards
  • Temporary bank or network issues
  • Card reported as lost or stolen
  • Card declined by issuing bank

Stripe provides detailed error messages that can help identify the specific reason for payment failure.

 

Step 2: Accessing Failed Invoices in Stripe Dashboard

 

To view failed invoices:

  1. Log in to your Stripe Dashboard (https://dashboard.stripe.com)
  2. Navigate to "Billing" > "Invoices" from the left sidebar
  3. Click on the "Failed" filter to see all invoices with failed payments
  4. Click on a specific invoice to view its details

 

Step 3: Manual Retry Through Stripe Dashboard

 

The simplest way to retry a failed payment is through the Stripe Dashboard:

  1. Open the failed invoice in your Stripe Dashboard
  2. Look for the "Retry payment" button in the invoice details
  3. Click "Retry payment" to attempt to charge the customer's payment method again
  4. Stripe will immediately attempt to process the payment using the customer's default payment method

 

Step 4: Updating Payment Details Before Retrying

 

If the payment failed due to invalid card information, you may want to update the customer's payment method before retrying:

  1. Navigate to "Customers" in the Stripe Dashboard
  2. Find and select the customer associated with the failed invoice
  3. Click on "Payment methods" tab
  4. Add a new payment method or update the existing one
  5. Return to the failed invoice and click "Retry payment"

 

Step 5: Retrying Payments Using Stripe API

 

For automated retries, you can use the Stripe API. Here's how to retry a payment for a specific invoice using the API:


const stripe = require('stripe')('sk_test_your_secret_key');

async function retryInvoicePayment(invoiceId) {
  try {
    const invoice = await stripe.invoices.pay(invoiceId, {
      // Optional: You can specify a different payment method here
      // payment_method: 'pm_123456789',
    });
    
    console.log('Payment retry successful:', invoice.id);
    return invoice;
  } catch (error) {
    console.error('Error retrying payment:', error.message);
    throw error;
  }
}

// Example usage
retryInvoicePayment('in\_1234567890');

 

Step 6: Retrying with a Different Payment Method via API

 

If you want to retry with a different payment method:


const stripe = require('stripe')('sk_test_your_secret_key');

async function retryWithDifferentPaymentMethod(invoiceId, newPaymentMethodId) {
  try {
    // Get the customer ID from the invoice
    const invoice = await stripe.invoices.retrieve(invoiceId);
    const customerId = invoice.customer;
    
    // Update the invoice's default payment method
    await stripe.invoices.update(invoiceId, {
      default_payment_method: newPaymentMethodId,
    });
    
    // Retry the payment
    const updatedInvoice = await stripe.invoices.pay(invoiceId);
    
    console.log('Payment retry successful with new method:', updatedInvoice.id);
    return updatedInvoice;
  } catch (error) {
    console.error('Error retrying payment with new method:', error.message);
    throw error;
  }
}

// Example usage
retryWithDifferentPaymentMethod('in_1234567890', 'pm_new_payment_method');

 

Step 7: Setting Up Automatic Retries

 

Stripe offers automatic retries for failed payments. You can configure this in your Stripe Dashboard:

  1. Go to "Settings" > "Billing" > "Subscriptions and Invoices"
  2. Under "Payment recovery", you can customize your retry schedule
  3. Set the number of retries (usually between 1-4) and the interval between retries
  4. Save your changes

Default retry schedule is typically 3, 5, 7 days after the initial failure.

 

Step 8: Implementing Smart Retries with Webhooks

 

You can implement a more sophisticated retry system using Stripe webhooks to listen for payment failure events:


const express = require('express');
const stripe = require('stripe')('sk_test_your_secret_key');
const app = express();

// Parse JSON body
app.use(express.json());

app.post('/webhook', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const endpointSecret = 'whsec_your_webhook\_secret';
  
  let event;
  
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle specific events
  if (event.type === 'invoice.payment\_failed') {
    const invoice = event.data.object;
    
    // Implement custom retry logic
    // Example: Check the number of previous attempts
    const attemptCount = invoice.attempt\_count;
    
    if (attemptCount < 3) {
      // Maybe wait and retry after a calculated delay
      // For demonstration, we're retrying immediately
      try {
        await stripe.invoices.pay(invoice.id);
        console.log(`Successfully retried payment for invoice ${invoice.id}`);
      } catch (error) {
        console.error(`Failed to retry payment: ${error.message}`);
      }
    } else {
      // Consider sending a notification to the customer
      console.log(`Maximum retry attempts reached for invoice ${invoice.id}`);
    }
  }
  
  res.status(200).send('Webhook received');
});

app.listen(3000, () => console.log('Webhook server running on port 3000'));

 

Step 9: Customizing Retry Logic Based on Failure Reason

 

Different failure reasons might warrant different retry strategies. Here's how to implement custom retry logic based on the failure code:


async function smartRetryStrategy(invoiceId) {
  try {
    // Get the invoice and check the last payment error
    const invoice = await stripe.invoices.retrieve(invoiceId);
    const lastError = invoice.last_payment_error;
    
    if (!lastError) {
      console.log('No payment error found');
      return;
    }
    
    // Create a retry strategy based on the error code
    switch (lastError.code) {
      case 'card\_declined':
        if (lastError.decline_code === 'insufficient_funds') {
          // Wait longer for insufficient funds (e.g., 5 days)
          setTimeout(() => retryInvoicePayment(invoiceId), 5 _ 24 _ 60 _ 60 _ 1000);
          console.log('Scheduled retry in 5 days for insufficient funds');
        } else {
          // For other card declines, maybe notify the customer to update their card
          console.log('Card declined for another reason. Customer notification required.');
        }
        break;
        
      case 'expired\_card':
        // No point in retrying with an expired card - notify customer
        console.log('Card expired. Customer notification required.');
        break;
        
      case 'processing\_error':
        // For processing errors, retry soon as it might be temporary
        setTimeout(() => retryInvoicePayment(invoiceId), 30 _ 60 _ 1000); // 30 minutes
        console.log('Scheduled retry in 30 minutes for processing error');
        break;
        
      default:
        // Generic retry strategy for other errors
        setTimeout(() => retryInvoicePayment(invoiceId), 24 _ 60 _ 60 \* 1000); // 1 day
        console.log('Scheduled generic retry in 1 day');
    }
    
  } catch (error) {
    console.error('Error implementing smart retry strategy:', error.message);
  }
}

// Example usage
smartRetryStrategy('in\_1234567890');

 

Step 10: Notifying Customers About Failed Payments

 

It's often helpful to notify customers when their payments fail, giving them the opportunity to update their payment information:


const nodemailer = require('nodemailer');
const stripe = require('stripe')('sk_test_your_secret_key');

async function notifyCustomerAboutFailedPayment(invoiceId) {
  try {
    // Get invoice details
    const invoice = await stripe.invoices.retrieve(invoiceId, {
      expand: ['customer'],
    });
    
    const customerEmail = invoice.customer.email;
    const amount = invoice.amount\_due / 100; // Convert from cents to dollars
    const currency = invoice.currency.toUpperCase();
    const paymentLink = `https://yourwebsite.com/update-payment?invoice=${invoiceId}`;
    
    // Set up email transporter (replace with your SMTP details)
    const transporter = nodemailer.createTransport({
      host: 'smtp.example.com',
      port: 587,
      secure: false,
      auth: {
        user: 'your\[email protected]',
        pass: 'your\_password',
      },
    });
    
    // Send email
    const info = await transporter.sendMail({
      from: '"Your Company" [[email protected]](mailto:[email protected])',
      to: customerEmail,
      subject: 'Your payment has failed',
      html: \`
        

Payment Failed

Dear customer,

We were unable to process your payment of ${amount} ${currency} for invoice ${invoice.number}.

To update your payment information and complete the payment, please click the link below:

Update Payment Information

If you have any questions, please contact our support team.

Thank you,
Your Company Team

\`, }); console.log('Notification email sent:', info.messageId); return info; } catch (error) { console.error('Error sending notification:', error.message); throw error; } } // Example usage notifyCustomerAboutFailedPayment('in\_1234567890');

 

Step 11: Creating a Customer Portal for Self-Service Payment Updates

 

Stripe offers a Customer Portal that allows customers to update their payment methods themselves:


const stripe = require('stripe')('sk_test_your_secret_key');

async function createCustomerPortalSession(customerId, returnUrl) {
  try {
    const session = await stripe.billingPortal.sessions.create({
      customer: customerId,
      return\_url: returnUrl,
    });
    
    return session.url;
  } catch (error) {
    console.error('Error creating portal session:', error.message);
    throw error;
  }
}

// Example route in an Express.js application
app.get('/create-customer-portal', async (req, res) => {
  const customerId = req.query.customer\_id;
  const returnUrl = 'https://yourwebsite.com/account';
  
  try {
    const portalUrl = await createCustomerPortalSession(customerId, returnUrl);
    res.redirect(portalUrl);
  } catch (error) {
    res.status(500).send(`Error: ${error.message}`);
  }
});

 

Step 12: Monitoring Retry Performance

 

It's important to track how effective your retry strategy is. Here's a simple script to analyze retry performance:


const stripe = require('stripe')('sk_test_your_secret_key');

async function analyzeRetryPerformance(startDate, endDate) {
  try {
    // Get all invoices that were paid after at least one retry
    const invoices = await stripe.invoices.list({
      created: {
        gte: Math.floor(new Date(startDate).getTime() / 1000),
        lte: Math.floor(new Date(endDate).getTime() / 1000),
      },
      status: 'paid',
      limit: 100, // Adjust as needed
    });
    
    // Filter and analyze invoices with retry attempts
    const retriedInvoices = invoices.data.filter(invoice => invoice.attempt\_count > 1);
    
    const totalRetried = retriedInvoices.length;
    const totalAmount = retriedInvoices.reduce((sum, invoice) => sum + invoice.amount\_paid, 0) / 100;
    const averageAttempts = retriedInvoices.reduce((sum, invoice) => sum + invoice.attempt\_count, 0) / totalRetried;
    
    return {
      period: `${startDate} to ${endDate}`,
      totalInvoicesRetried: totalRetried,
      totalAmountRecovered: `$${totalAmount.toFixed(2)}`,
      averageAttemptsBeforeSuccess: averageAttempts.toFixed(2),
      recoveryRate: `${((totalRetried / invoices.data.length) * 100).toFixed(2)}%`
    };
  } catch (error) {
    console.error('Error analyzing retry performance:', error.message);
    throw error;
  }
}

// Example usage
analyzeRetryPerformance('2023-01-01', '2023-12-31')
  .then(report => console.log('Retry Performance Report:', report))
  .catch(error => console.error('Analysis failed:', error));

 

Conclusion

 

Retrying failed invoice payments in Stripe can be done manually through the Dashboard or programmatically via the API. For best results, consider:

  • Setting up automatic retries with a sensible schedule
  • Notifying customers about payment failures
  • Providing easy ways for customers to update their payment information
  • Implementing custom retry logic based on failure reasons
  • Monitoring the performance of your retry strategy

By following these steps, you can significantly improve your payment recovery rate and reduce revenue loss from failed payments.

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