Dunning emails: what to send when a payment fails

A dunning email tells a customer that a payment failed and how to fix it. This guide shows a four-email sequence and two ways to send it from Stripe events in Loops, by hand or with an AI agent.

What a dunning email is

A dunning email goes out after a subscription payment fails. It says what failed, links to where the customer can fix it, and says what happens next. A dunning sequence spreads a few of these across your payment processor’s retry window.

The customer didn’t choose to leave: a card expired or a bank declined the charge. Stripe calls this involuntary churn and notes that many failed payments are recoverable.

Two ways to send them

Both start from Stripe’s invoice.payment_failed event. The difference is how much the email can say.

Set it up with an AI agent

Connect Claude, ChatGPT, or Codex through the Loops MCP server, or give Claude Code or Cursor the Loops CLI and agent skills. Then paste a prompt:

Send the eventIn our Stripe webhook handler, send a paymentFailed event to Loops on the first failed attempt, with the amount and invoice link as event properties.
Build the workflowSet up a failed-payment workflow in Loops triggered by my Stripe webhook’s paymentFailed event: a first notice, a 3-day timer, a filter on subscriptionStatus, a reminder, and a final notice. Keep it as a draft.
Write the emailsWrite the three dunning emails in LMX with the amount and invoice link from the event, then send me previews.

The agent edits your webhook handler, sends a test event, builds the workflow as a draft with the Workflows API, and writes each email in LMX. You review the draft in Loops and start it.

The workflow endpoints are in alpha, so check the draft before you start it. Stripe events carry only the customer’s email and name, so the amount and invoice link come from your app’s event. See Loops for AI agents.

A four-email dunning sequence

Stripe retries a failed subscription payment on its own, and the Smart Retries default is 8 tries in 2 weeks. Spread your emails across that window and stop when the invoice is paid.

1
Day 0Payment failed

The first failed attempt. Your app sends paymentFailed and the workflow starts.

A
Your payment didn’t go throughAcme
Acme

We couldn’t process your payment

Hi First Name, your latest payment for Acme didn’t go through. Update your payment method and we’ll use it for the next attempt.

Amount dueamountDue currency
Payment method•••• •••• Update needed
Update payment method
Already updated it? You can ignore this email.
AcmeQuestions? Just reply to this email.
2
Day 3Reminder

A 3-day timer, then an audience filter. Anyone who paid has already left.

A
Reminder: update your payment methodAcme
Acme

Your invoice is still unpaid

Hi First Name, we still haven’t been able to collect your payment. It takes about a minute to fix.

Amount dueamountDue currency
Update payment method
AcmeQuestions? Just reply to this email.
3
Before retries endFinal notice

Another timer. Name a date only if your app knows it.

A
Last reminder before we cancelAcme
Acme

Your subscription is about to end

Hi First Name, we’ve tried your payment several times. If it isn’t resolved before our last try, we’ll cancel your Acme subscription.

Amount dueamountDue currency
Keep my subscription
AcmeQuestions? Just reply to this email.
4
After cancellationSubscription ended

A separate workflow on Stripe’s cancellation event, which only carries the name.

A
Your Acme subscription has endedAcme
Acme

Your subscription has ended

Hi First Name, your Acme subscription has ended. You can restart it from your billing page whenever you’re ready.

Restart subscription
AcmeQuestions? Just reply to this email.
Emails 1 to 3 come from one workflow on your app’s paymentFailed event, so they can use its amountDue and currency properties. Email 4 comes from Stripe’s cancellation event, which only carries the customer’s name.

When retries run out, Stripe cancels the subscription, marks it unpaid, leaves it past due, or pauses it, depending on your settings. The Loops lifecycle recipe suggests one to three emails during the dunning period.

What to put in a dunning email

A
Your payment didn’t go throughAcme
Acme

We couldn’t process your payment

Hi First Name, your latest payment for Acme didn’t go through.

says what failed
Amount dueamountDue currency
the amount due
Update payment methodone button to fix it
We’ll try the payment again automatically.
what happens next
AcmeQuestions? Just reply to this email.
a person reads replies
Tags like First Name and amountDue are filled in when the email sends.

Keep the subject plain and leave out promotions. A hard decline, such as a lost or stolen card, won’t be retried until there’s a new payment method, so the update button does most of the work.

If Stripe’s own failed payment emails are on, turn them off or drop your first email, so nobody gets the same notice twice.

Option 1: the Loops Stripe app

Connect Stripe to Loops, turn on invoice.payment_failed, and create a workflow from the Stripe - Payment Failed template. Its trigger is Event received, then Stripe, then invoice.payment_failed.

Loops matches the customer by email and can sync their name. That’s all a Stripe event carries, so leave out the amount and point the button at a billing page that shows the balance.

Stripe sends invoice.payment_failed after every failed attempt. Set the trigger to Every time and keep the workflow to one email, or a longer sequence can restart on each retry. To try it, Start for free and connect Stripe from Settings.

Option 2: your own event

Your app sends a paymentFailed event on the first failure for each invoice, with the amount and invoice link as event properties, and sets the subscriptionStatus contact property to Failed. When the invoice is paid, it sets Paying.

Event received
paymentFailed
Send email
Payment failed
Timer
Wait 3 days
Audience filter
by subscriptionStatus
still Failed? they go on
paid? they leave here
Send email
Reminder
A customer who pays during the wait never gets the reminder.

Set the trigger to Every time and the audience filter to All following nodes. If one customer can have two unpaid invoices at once, keep recovery state per invoice, as the lifecycle email guide explains.

Send the event from your webhook

Call this with a Stripe event your route has verified. It sends paymentFailed once per invoice and treats a 409 as already sent.

import { LoopsClient, APIError } from "loops";

const loops = new LoopsClient(process.env.LOOPS_API_KEY);

// Call with a Stripe event your webhook route has verified.
export async function syncPaymentState(event) {
  const invoice = event.data.object;
  const email = invoice.customer_email;
  if (!email) return;
  if (event.type === "invoice.paid") {
    return loops.updateContact({ email, properties: { subscriptionStatus: "Paying" } });
  }
  if (event.type !== "invoice.payment_failed" || invoice.attempt_count !== 1) return;
  try {
    await loops.sendEvent({
      email,
      eventName: "paymentFailed",
      contactProperties: { subscriptionStatus: "Failed" },
      eventProperties: {
        amountDue: (invoice.amount_due / 100).toFixed(2),
        currency: invoice.currency.toUpperCase(),
        invoiceUrl: invoice.hosted_invoice_url,
      },
      headers: { "Idempotency-Key": event.id },
    });
  } catch (err) {
    if (!(err instanceof APIError && err.statusCode === 409)) throw err;
  }
}

amount_due is in the smallest currency unit, so dividing by 100 works only for two-decimal currencies. Create the subscriptionStatus property in Loops first, and if paymentFailed already exists, add its new properties before you send them.

Workflow or transactional email?

Workflow emailReachesSubscribed contacts onlyTrackingOpens and clicksSet upIn Loops, no code with the Stripe app
Transactional emailReachesEvery customer, unsubscribed tooTrackingNoneSet upSent from your server with data variables

Send a notice as transactional email when it has to reach every customer, and keep it to the payment problem. Send Stripe failed payment emails shows the server side.

Measure recovered payments, not opens

Count the failed invoices that end up paid. Stripe’s recovery analytics show failure and recovery rates. Workflow emails track opens and clicks and transactional email doesn’t, but neither proves an email caused a payment.

Common questions

What is a dunning email?

An email sent after a subscription payment fails. It says which payment failed, links to where the customer can update their payment method, and says what happens next. A dunning sequence is a short series of these across the payment retry window.

How many dunning emails should I send?

Plan for three while payments are being retried: one when the payment fails, a reminder a few days later, and a final notice before the subscription changes. Send one more after it ends if you want to explain how to restart. Stop as soon as the payment succeeds.

Should dunning emails be transactional or marketing email?

In Loops, workflow emails only reach subscribed contacts, and transactional email also reaches contacts who unsubscribed. Use a workflow when the notice can follow marketing preferences. Send transactional email from your server when every customer must get it, and keep it to the payment issue.

Can Loops include the invoice amount from Stripe?

Not from the Stripe app alone. Stripe events in Loops sync the customer’s email address and, optionally, their name. To include the amount or the invoice link, have your app send its own event with event properties, or send a transactional email with data variables.

Can an AI agent set up dunning emails in Loops?

Yes. Through the Loops MCP server or the CLI, an agent can send the event your app needs, create the workflow as a draft with its trigger, timers, and audience filter, and write the emails in LMX. You review the draft and start it in Loops.

Should I turn off Stripe’s failed payment emails?

Only if your Loops emails replace them. Stripe can email customers after each failed payment, so with both on, a customer gets two notices for the same failure. Pick one sender for each message.

What's next

More on product and lifecycle email.

See all guides