Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to retry failed invoice payments in Stripe

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.

What you'll learn

  • How to enable and configure Stripe Smart Retries
  • How to set up dunning emails for failed payments
  • How to manually retry invoices via the API
  • How to listen for payment failure webhooks and take action
  • How to build a failed payment recovery workflow
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read20 minutesStripe API v2024-12+, Node.js 18+March 2026RapidDev Engineering Team
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.

Prerequisites

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

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3async function retryInvoicePayment(invoiceId) {
4 try {
5 const invoice = await stripe.invoices.pay(invoiceId);
6 console.log(`Invoice ${invoice.id} payment status: ${invoice.status}`);
7 return invoice;
8 } catch (err) {
9 console.error(`Retry failed for invoice ${invoiceId}:`, err.message);
10 throw err;
11 }
12}

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.

typescript
1app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
2 const sig = req.headers['stripe-signature'];
3 let event;
4
5 try {
6 event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
7 } catch (err) {
8 return res.status(400).send(`Webhook Error: ${err.message}`);
9 }
10
11 if (event.type === 'invoice.payment_failed') {
12 const invoice = event.data.object;
13 const attemptCount = invoice.attempt_count;
14 console.log(`Payment failed for customer ${invoice.customer}, attempt #${attemptCount}`);
15
16 // Custom logic: notify user, restrict access after N failures, etc.
17 if (attemptCount >= 3) {
18 // Flag account for manual review or restrict features
19 }
20 }
21
22 res.json({ received: true });
23});

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.

typescript
1# Create a customer with a card that fails then succeeds
2const customer = await stripe.customers.create({
3 email: 'test@example.com',
4 source: 'tok_chargeCustomerFail', // fails first, succeeds on retry
5});
6
7const subscription = await stripe.subscriptions.create({
8 customer: customer.id,
9 items: [{ price: 'price_your_test_price_id' }],
10});

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 working example

retry-handler.js
1require('dotenv').config();
2const express = require('express');
3const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
4
5const app = express();
6
7// Webhook endpoint for payment failure events
8app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
9 const sig = req.headers['stripe-signature'];
10 let event;
11
12 try {
13 event = stripe.webhooks.constructEvent(
14 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
15 );
16 } catch (err) {
17 console.error('Signature verification failed:', err.message);
18 return res.status(400).send(`Webhook Error: ${err.message}`);
19 }
20
21 if (event.type === 'invoice.payment_failed') {
22 const invoice = event.data.object;
23 console.log(`Payment failed — invoice: ${invoice.id}, attempt: ${invoice.attempt_count}`);
24
25 if (invoice.attempt_count >= 3) {
26 console.log(`Flagging customer ${invoice.customer} for manual review`);
27 // TODO: restrict access, send urgent notification
28 }
29 }
30
31 if (event.type === 'invoice.payment_succeeded') {
32 const invoice = event.data.object;
33 console.log(`Payment recovered — invoice: ${invoice.id}`);
34 // TODO: restore access if previously restricted
35 }
36
37 res.json({ received: true });
38});
39
40app.use(express.json());
41
42// Manual retry endpoint
43app.post('/retry-invoice/:invoiceId', async (req, res) => {
44 try {
45 const invoice = await stripe.invoices.pay(req.params.invoiceId);
46 res.json({ status: invoice.status, id: invoice.id });
47 } catch (err) {
48 res.status(400).json({ error: err.message, code: err.code });
49 }
50});
51
52// List failed invoices for a customer
53app.get('/failed-invoices/:customerId', async (req, res) => {
54 try {
55 const invoices = await stripe.invoices.list({
56 customer: req.params.customerId,
57 status: 'open',
58 limit: 10,
59 });
60 res.json(invoices.data.map(inv => ({
61 id: inv.id,
62 amount_due: inv.amount_due,
63 currency: inv.currency,
64 attempt_count: inv.attempt_count,
65 next_payment_attempt: inv.next_payment_attempt,
66 })));
67 } catch (err) {
68 res.status(400).json({ error: err.message });
69 }
70});
71
72const PORT = process.env.PORT || 3000;
73app.listen(PORT, () => console.log(`Retry handler running on port ${PORT}`));

Common mistakes when retryying failed invoice payments in Stripe

Why it's a problem: Not enabling Smart Retries and relying only on a fixed retry schedule

How to avoid: Enable Smart Retries in Dashboard → Settings → Billing → Manage failed payments. ML-optimized timing recovers significantly more revenue than fixed intervals.

Why it's a problem: Canceling subscriptions immediately after the first failed payment

How to avoid: Configure a retry window of 1-4 weeks and set subscriptions to 'past_due' before canceling. Most failed payments recover within the retry window.

Why it's a problem: Not sending any notification to customers about failed payments

How to avoid: Enable Stripe dunning emails and supplement them with in-app notifications. Customers often just need to update an expired card.

Why it's a problem: Retrying payments too aggressively via the API

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write a Node.js Express server that handles Stripe webhook events for invoice.payment_failed and invoice.payment_succeeded. Include a manual retry endpoint using stripe.invoices.pay() and a route to list open invoices for a customer. Use raw body parsing for webhook signature verification.

Stripe Prompt

Build a Stripe failed payment recovery system in Node.js. Handle invoice.payment_failed webhooks with signature verification, provide a manual retry endpoint, and list open invoices per customer. Use express.raw() for the webhook route.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.