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
Enable Smart Retries in the Stripe Dashboard
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.
Configure the retry schedule and subscription behavior
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.
Set up dunning emails
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.
Retry a specific invoice via the API
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.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23async 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.
Listen for payment failure webhooks
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.
1app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {2 const sig = req.headers['stripe-signature'];3 let event;45 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 }1011 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}`);1516 // Custom logic: notify user, restrict access after N failures, etc.17 if (attemptCount >= 3) {18 // Flag account for manual review or restrict features19 }20 }2122 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.
Test the retry flow with test cards
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.
1# Create a customer with a card that fails then succeeds2const customer = await stripe.customers.create({3 email: 'test@example.com',4 source: 'tok_chargeCustomerFail', // fails first, succeeds on retry5});67const 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
1require('dotenv').config();2const express = require('express');3const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);45const app = express();67// Webhook endpoint for payment failure events8app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {9 const sig = req.headers['stripe-signature'];10 let event;1112 try {13 event = stripe.webhooks.constructEvent(14 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET15 );16 } catch (err) {17 console.error('Signature verification failed:', err.message);18 return res.status(400).send(`Webhook Error: ${err.message}`);19 }2021 if (event.type === 'invoice.payment_failed') {22 const invoice = event.data.object;23 console.log(`Payment failed — invoice: ${invoice.id}, attempt: ${invoice.attempt_count}`);2425 if (invoice.attempt_count >= 3) {26 console.log(`Flagging customer ${invoice.customer} for manual review`);27 // TODO: restrict access, send urgent notification28 }29 }3031 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 restricted35 }3637 res.json({ received: true });38});3940app.use(express.json());4142// Manual retry endpoint43app.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});5152// List failed invoices for a customer53app.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});7172const 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation