# How to retry failed invoice payments in Stripe

- Tool: Stripe
- Difficulty: Intermediate
- Time required: 20 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+
- Last updated: March 2026

## TL;DR

Stripe offers Smart Retries that use machine learning to retry failed invoice payments at optimal times. You can also configure manual retry schedules, set up dunning emails to notify customers, and use the API to programmatically retry specific invoices. Combining automatic retries with customer communication recovers up to 40% of failed payments.

## Why Failed Payment Retries Matter for Subscription Revenue

Failed payments are the number one cause of involuntary churn in SaaS businesses. Card declines, expired cards, and insufficient funds account for 20-40% of subscription cancellations. Stripe Smart Retries use machine learning to determine the best time to retry a failed charge, recovering a significant portion of revenue automatically. Combined with dunning emails and manual retry logic, you can minimize churn and keep your subscription revenue stable.

## Before you start

- A Stripe account with at least one active subscription in test mode
- Access to the Stripe Dashboard billing settings
- Node.js 18+ installed for API-based retry examples
- Understanding of Stripe webhooks for payment failure notifications

## Step-by-step guide

### 1. Enable Smart Retries in the Stripe Dashboard

Go to Stripe Dashboard → Settings → Billing → Subscriptions and emails → Manage failed payments. Enable 'Use Smart Retries'. Stripe will automatically retry failed payments using ML-optimized timing over a configurable retry window (default: up to 4 weeks).

**Expected result:** Smart Retries enabled. Stripe will automatically retry failed subscription payments at ML-determined optimal times.

### 2. Configure the retry schedule and subscription behavior

In the same Manage failed payments section, configure what happens after all retries fail: mark the subscription as unpaid, cancel it, or leave it past_due. Set the retry window (1-4 weeks). Choose whether to email the customer on each retry attempt.

**Expected result:** Retry window and post-failure behavior configured. Subscriptions will follow your defined rules after exhausting retry attempts.

### 3. Set up dunning emails

Under Settings → Billing → Subscriptions and emails → Email customers, enable the failed payment notification emails. Stripe sends automated emails to customers when payments fail, including a link to update their payment method via the Stripe-hosted customer portal.

**Expected result:** Customers receive automated emails when their payment fails, with a link to update their card.

### 4. Retry a specific invoice via the API

Use the Stripe API to manually retry payment on a specific invoice. This is useful for building a custom retry flow triggered by your own logic or customer actions.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

async function retryInvoicePayment(invoiceId) {
  try {
    const invoice = await stripe.invoices.pay(invoiceId);
    console.log(`Invoice ${invoice.id} payment status: ${invoice.status}`);
    return invoice;
  } catch (err) {
    console.error(`Retry failed for invoice ${invoiceId}:`, err.message);
    throw err;
  }
}
```

**Expected result:** The invoice payment is retried. If successful, the invoice status changes to 'paid'. If it fails, Stripe returns the decline reason.

### 5. Listen for payment failure webhooks

Set up a webhook handler for invoice.payment_failed events. This lets you trigger custom logic like sending in-app notifications, flagging accounts, or escalating to support. Teams at RapidDev often build these recovery flows to reduce involuntary churn for their clients.

```
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'invoice.payment_failed') {
    const invoice = event.data.object;
    const attemptCount = invoice.attempt_count;
    console.log(`Payment failed for customer ${invoice.customer}, attempt #${attemptCount}`);

    // Custom logic: notify user, restrict access after N failures, etc.
    if (attemptCount >= 3) {
      // Flag account for manual review or restrict features
    }
  }

  res.json({ received: true });
});
```

**Expected result:** Your server receives payment failure events in real time and executes custom recovery logic based on the number of failed attempts.

### 6. Test the retry flow with test cards

Use Stripe test cards to simulate payment failures. Card 4000000000000341 always fails on the first charge attempt but succeeds on retry. Card 4000000000009995 always declines with insufficient_funds. Create a test subscription and observe the retry behavior.

```
# Create a customer with a card that fails then succeeds
const customer = await stripe.customers.create({
  email: 'test@example.com',
  source: 'tok_chargeCustomerFail',  // fails first, succeeds on retry
});

const subscription = await stripe.subscriptions.create({
  customer: customer.id,
  items: [{ price: 'price_your_test_price_id' }],
});
```

**Expected result:** The first payment attempt fails. Smart Retries (or manual retry) succeed on the second attempt. Invoice status changes from 'open' to 'paid'.

## Complete code example

File: `retry-handler.js`

```javascript
require('dotenv').config();
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();

// Webhook endpoint for payment failure events
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, process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    console.error('Signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'invoice.payment_failed') {
    const invoice = event.data.object;
    console.log(`Payment failed — invoice: ${invoice.id}, attempt: ${invoice.attempt_count}`);

    if (invoice.attempt_count >= 3) {
      console.log(`Flagging customer ${invoice.customer} for manual review`);
      // TODO: restrict access, send urgent notification
    }
  }

  if (event.type === 'invoice.payment_succeeded') {
    const invoice = event.data.object;
    console.log(`Payment recovered — invoice: ${invoice.id}`);
    // TODO: restore access if previously restricted
  }

  res.json({ received: true });
});

app.use(express.json());

// Manual retry endpoint
app.post('/retry-invoice/:invoiceId', async (req, res) => {
  try {
    const invoice = await stripe.invoices.pay(req.params.invoiceId);
    res.json({ status: invoice.status, id: invoice.id });
  } catch (err) {
    res.status(400).json({ error: err.message, code: err.code });
  }
});

// List failed invoices for a customer
app.get('/failed-invoices/:customerId', async (req, res) => {
  try {
    const invoices = await stripe.invoices.list({
      customer: req.params.customerId,
      status: 'open',
      limit: 10,
    });
    res.json(invoices.data.map(inv => ({
      id: inv.id,
      amount_due: inv.amount_due,
      currency: inv.currency,
      attempt_count: inv.attempt_count,
      next_payment_attempt: inv.next_payment_attempt,
    })));
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Retry handler running on port ${PORT}`));
```

## Common mistakes

- **Not enabling Smart Retries and relying only on a fixed retry schedule** — undefined Fix: Enable Smart Retries in Dashboard → Settings → Billing → Manage failed payments. ML-optimized timing recovers significantly more revenue than fixed intervals.
- **Canceling subscriptions immediately after the first failed payment** — undefined Fix: Configure a retry window of 1-4 weeks and set subscriptions to 'past_due' before canceling. Most failed payments recover within the retry window.
- **Not sending any notification to customers about failed payments** — undefined Fix: Enable Stripe dunning emails and supplement them with in-app notifications. Customers often just need to update an expired card.
- **Retrying payments too aggressively via the API** — undefined Fix: Avoid calling stripe.invoices.pay() repeatedly in a loop. Let Smart Retries handle the timing. Only use manual retry when the customer takes action (e.g., updates their card).

## Best practices

- Enable Smart Retries to let Stripe ML determine the optimal retry timing
- Set the retry window to at least 2 weeks to maximize recovery chances
- Configure dunning emails so customers are notified about failed payments
- Listen for invoice.payment_failed webhooks to trigger custom in-app notifications
- Provide a self-service portal where customers can update their payment method
- Track attempt_count on invoices to escalate after multiple failures
- Restore full access immediately when a payment recovery succeeds
- Monitor your involuntary churn rate in Stripe Revenue reports

## Frequently asked questions

### How long does Stripe retry failed payments?

By default, Stripe retries failed subscription payments for up to 4 weeks. You can configure this retry window from 1 to 4 weeks in Dashboard → Settings → Billing → Manage failed payments.

### What is Smart Retries in Stripe?

Smart Retries is a Stripe feature that uses machine learning to determine the optimal time to retry a failed payment. It analyzes patterns across the Stripe network to choose retry times with the highest success probability, recovering about 11% more revenue than fixed schedules.

### Can I manually retry a failed invoice via the API?

Yes. Call stripe.invoices.pay(invoiceId) to immediately retry payment on a specific invoice. This is useful when a customer updates their payment method and you want to charge them right away.

### What happens to a subscription after all retries fail?

The behavior depends on your settings. You can configure Stripe to cancel the subscription, mark it as 'unpaid', or leave it as 'past_due'. Configure this in Dashboard → Settings → Billing → Manage failed payments.

### How do dunning emails work in Stripe?

Stripe can automatically email customers when their payment fails. The email includes details about the failure and a link to the Stripe customer portal where they can update their payment method. Enable this in Dashboard → Settings → Billing → Subscriptions and emails.

### What test cards can I use to simulate failed payments?

Use 4000000000000341 for a card that fails the first charge but succeeds on retry. Use 4000000000009995 for an insufficient_funds decline. Use 4242424242424242 for a card that always succeeds.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-retry-failed-invoice-payments-in-stripe
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-retry-failed-invoice-payments-in-stripe
