Recover the Stripe payments that failed on the card
Stripe already retries the card, so do not build a schedule. Branch on the attempt number that arrives with each invoice.payment_failed event, escalate the tone across the three attempts, append the hosted payment link in code rather than letting a model write it, and alert yourself only on the last one.
A good share of churn is not a decision at all. The customer is not comparing you to a competitor, their bank just issued a new card and nobody told your billing system. That is usually the cheapest revenue you can get back.
You do not need a schedule, Stripe already has one
The instinct is to catch the first failure and then build a sequence with waits: email now, wait three days, email again. That is the wrong shape, and it fights the thing it is sitting on top of.
Stripe retries the card on its own cadence and emits invoice.payment_failed every single time. The invoice carries attempt_count, so the event tells you where in the sequence you are:
attempt_count: 1 -> first failure -> assume a card expiry, stay light
attempt_count: 2 -> repeat failure -> be direct, action is needed
attempt_count: 3 -> final attempt -> access is at risk, warmlyBuild your own waits on top and you have two schedules disagreeing, which produces emails saying we will try again tomorrow on days when Stripe will not. Branch on the counter instead and each message is correct by construction.
The invoice also carries next_payment_attempt. When it is present you can tell the customer exactly when the retry happens. When it is absent, that was the last automatic attempt, and the email should say so rather than promising another try.
The bug that emails someone the wrong price
Stripe quotes amounts in minor units, so 4900 means 49.00 and you divide by a hundred. Except for zero-decimal currencies, where 5000 means 5000 and dividing gives you 50.
const zeroDecimal = ['JPY','KRW','VND','CLP','ISK','UGX','XAF','XOF',
'BIF','DJF','GNF','KMF','MGA','PYG','RWF','VUV','XPF'];
const amount = zeroDecimal.includes(currency)
? minor // 5000 JPY is 5000 JPY
: minor / 100; // 4900 USD is 49.00 USDMiss it and you email a Japanese customer asking for a hundredth of what they owe. Four lines to get right, and it will not show up in testing unless you deliberately test a yen invoice.
Let AI write the sentence, never the link
A model is good at writing three genuinely different emails from one set of facts, which is the tedious part. It is the wrong tool for the two pieces of data that have to be exact.
- The payment link is appended in code. Take
hosted_invoice_urlfrom the invoice and add it after the body. Tell the model explicitly not to include a link, because a plausible invented URL in a payment email is indistinguishable from phishing. - The amount is passed as a formatted string the model is instructed to reproduce exactly, never as a number for it to format.
- Nothing else is invented. No dates, no discounts, no account details. If you did not pass it in, it does not appear.
- Fall back rather than fail. If the model returns nothing usable, send a plain templated email with the right amount and the right link. A recovery email that fails to send is lost revenue, so this path must never error out.
On tone, the whole thing rests on one assumption: the card expired. Never imply they cancelled, never shame, never threaten. They have not made a decision yet, and treating a bank event as a relationship event is how a recoverable payment turns into a real one.
Alert yourself once, at the end
Notify yourself on every failed charge and you will filter the sender within a fortnight, which leaves you where you started. One alert on the final attempt lands when the automation has run out and there is still time to do something.
Make that alert about the decision rather than the event. The amount, the customer, whether it is a subscription or a one-off, and whether any retry remains. For a large enough account, a personal email at that point recovers a real share of what the sequence missed.
Who this is actually for, and what changes
The mechanics are the same everywhere, but what you are protecting differs, and that changes the tone and the escalation point.
SaaS with monthly subscriptions
The problem. A failed card silently downgrades or locks an account. The customer discovers it when they try to log in, which turns a billing hiccup into a support ticket and a bad first impression of the week.
What to change. Send the first email before anything is restricted, and say plainly that access continues while you retry. Escalate to at-risk language only on the final attempt. The whole sequence should finish before the account state changes, not after.
Agencies and retainers
The problem. An automated third notice landing in a client inbox mid-project reads badly, and usually nobody on the account team knows it went out.
What to change. Route the internal alert on the first failure rather than the last, and consider suppressing the third email entirely for accounts over a threshold. For a large retainer the right recovery is a phone call, and the workflow's job is to make sure someone knows to make it.
Ecommerce subscriptions and boxes
The problem. A failed charge means a shipment does not go out. The customer notices the missing delivery long before they notice the email, and by then they have already decided you are unreliable.
What to change. Lead with the fulfilment consequence rather than the payment. Your next box is on hold until the card is updated is concrete and acts faster than a payment failed notice, because it names something the customer actually wants.
Usage-based and invoiced B2B
The problem. The person whose card is on file is often not the person who can fix it. The email goes to a founder who forwards it to finance three days later, by which point two retries have already failed.
What to change. Make the first email forwardable on purpose. State the amount, the invoice number and the link plainly, with no personal framing, so it survives being passed on without editing. Copy a billing contact if you hold one.
The shape of it
Stripe Trigger: invoice.payment_failed
-> Set config (brand, sender, which attempt counts as final)
-> Code: read the invoice, work out the stage, format the amount
-> Switch on stage
first -> Angle: assume a card expiry
second -> Angle: action is needed
final -> Angle: access is at risk
-> Code + AI: write the email in that register
-> Code: append the real payment link
-> Gmail: send to the customer
-> IF final -> Gmail: alert you before it is written offThe three angle nodes sit on the canvas as plain Set nodes, so the tone of each attempt is edited where you can see it rather than buried in code.
This is a free n8n template you can import and point at your own Stripe account. It handles zero-decimal currencies, invoices with no customer email, and the case where no further retry is scheduled.
Failed payment questions, straight answers.
What is involuntary churn?
Revenue you lose without the customer deciding to leave. The card expired, was replaced after a fraud alert, or hit a limit. It commonly runs somewhere between a fifth and two fifths of total churn. Because nothing is wrong with the product, the fix is plumbing.
Does Stripe not already recover failed payments?
Stripe retries the card on its own schedule, and Smart Retries alone recovers a large share of failed charges. What it gives you on the email side is a generic notice with limited control over timing and tone. Published figures put a single dunning email at roughly fifteen percent recovery and a properly sequenced set of four nearer thirty-five to forty-five. Stripe has the retries covered. The sequence is the part you control.
Do you need a Wait node to space the emails out?
No, and adding one is the most common mistake in these builds. Stripe fires invoice.payment_failed once per retry attempt and puts attempt_count on the invoice, so the webhook itself tells you where you are in the sequence. Adding your own waits means two schedules fighting each other and customers getting emails that contradict Stripe's own timing.
How many recovery emails should you send?
Three is a sensible default because it matches how many times Stripe will typically retry before giving up. What matters is the escalation rather than the count. Assume a card expiry on the first, be direct on the second, say plainly that access is at risk on the last. Sending the same message three times is barely better than sending it once.
What is the biggest technical mistake in a dunning build?
Dividing every amount by a hundred. Most currencies are quoted by Stripe in minor units, but zero-decimal currencies like JPY and KRW are not, so dividing turns 5000 yen into 50. You will email a customer a figure a hundred times smaller than what they owe, and the first time you find out is when they reply about it.
Should AI write the recovery email?
It can write the prose, but it must never write the payment link or the amount. Append the hosted invoice URL in code and pass the amount as a formatted string it is told to reproduce exactly. If the model invents a plausible looking link, you have sent something that reads like phishing, which is worse than losing the payment.
What tone actually recovers the payment?
Assume the card expired, because it usually did. Never imply the customer chose to leave, never shame, never threaten. The person has not decided anything yet, and treating a bank event as if it were a relationship problem is how you create one.
How do you know when to step in manually?
Alert yourself on the final attempt only. Getting a notification for every failed charge trains you to ignore them, whereas one message at the point where automation has run out is a prompt you will act on. On a big subscription a personal email at that moment tends to recover a decent slice of what the sequence missed.
How much of your churn is just cards?
30 minutes, free. Bring your Stripe account. You leave with a plan for a recovery sequence that escalates properly and tells you before a subscription is written off, whether you build it or we do.
Keep reading.
See every thread waiting on them
Has this thread had a reply is the wrong test and it hides your best leads. Compare sent against received per thread, then draft the nudge instead of sending it.
Stop finding out from the client
One error workflow for everything you run. Suppresses repeat alerts so a broken workflow sends one email not sixty, and sends the raw error even when the diagnosis fails.
Know which ad to scale, and by how much
Spend and result floors, a margin over the account average, frequency headroom and budget cap checks, then a sized raise.
Rather have it built for you?
Skip the build. A free 30-minute call and we set this up in your stack, live in a week or two.