Free email template
Payment failed email template
Dunning email for a declined card: what failed, when it retries, and one fix button. Free and MIT-licensed, in React Email, MJML, HTML and plain text, with light and dark mode built in.
- MIT license
- Works with any email provider
- Light and dark mode
What every payment failed email needs.
This template does all six. If you write your own, check it against this list.
What failed
Which card, for which plan, and how much. People with several subscriptions need to know which one this is.
No blame
Usually a card expired or the bank declined the charge. Say that, and skip "you failed to pay". The person reading is a customer you want to keep.
When you'll try again
The next attempt date tells people how long they have, and that nothing has been cancelled yet.
One fix button
Update the payment method. One button to the billing page, and nothing else to click.
What happens next
Say plainly what fixing it keeps: the template says "to keep your account active". No threats, no countdowns.
Never ask for card details by email
Phishers copy billing emails like this one. The button goes to your site; never ask people to reply with card numbers.
The code. Copy the format your stack uses.
Change the brand name, logo and colors once in theme.ts and every template follows.
import * as React from 'react'
import { Button, Details, Layout, P, Small, Title } from '../../components'
import type { TemplateMeta } from '../../components/meta'
import { plainText, textDetails } from '../../components/text'
export type PaymentFailedProps = {
name: string
planName: string
amount: string
paymentMethod: string
retryDate: string
updateUrl: string
}
export const meta: TemplateMeta = {
name: 'Payment failed',
description: 'Dunning email for a declined card: what failed, when it retries, and one fix button.',
group: 'Billing',
subject: 'Your payment didn’t go through',
}
export default function PaymentFailed(props: PaymentFailedProps) {
const { name, planName, amount, paymentMethod, retryDate, updateUrl } = props
return (
<Layout
preview={`We couldn't charge ${paymentMethod}. Update it before ${retryDate} to keep your plan.`}
footerNote="You're receiving this because a payment for your subscription failed."
>
<Title>Your payment didn't go through</Title>
<P>
Hi {name}, we couldn't charge your {paymentMethod} for your {planName} plan. This usually happens when a
card expires or the bank declines the charge.
</P>
<Details
rows={[
['Plan', planName],
['Amount due', amount],
['Next attempt', retryDate],
]}
/>
<P>Update your payment method before {retryDate} to keep your account active.</P>
<Button href={updateUrl}>Update payment method</Button>
<Small style={{ margin: 0 }}>Already updated your details? You can ignore this email.</Small>
</Layout>
)
}
PaymentFailed.PreviewProps = {
name: 'Ada',
planName: 'Pro',
amount: '$54.13',
paymentMethod: 'Visa ending in 4242',
retryDate: 'October 19, 2026',
updateUrl: 'https://example.com/billing/payment-method',
} satisfies PaymentFailedProps
export function text(props: PaymentFailedProps) {
const { name, planName, amount, paymentMethod, retryDate, updateUrl } = props
return plainText(
"Your payment didn't go through",
`Hi ${name}, we couldn't charge your ${paymentMethod} for your ${planName} plan. This usually happens when a card expires or the bank declines the charge.`,
textDetails([
['Plan', planName],
['Amount due', amount],
['Next attempt', retryDate],
]),
`Update your payment method before ${retryDate} to keep your account active:`,
updateUrl,
'Already updated your details? You can ignore this email.',
)
}
<mjml lang="en" dir="ltr">
<mj-head>
<mj-title>Your payment didn't go through</mj-title>
<mj-preview>We couldn't charge {{payment_method}}. Update it before {{retry_date}} to keep your plan.</mj-preview>
<!-- theme:start -->
<!-- Generated from theme.ts by `npm run build`. Edit theme.ts, not this block. -->
<mj-raw>
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
</mj-raw>
<mj-attributes>
<mj-all font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" />
<mj-body background-color="#f4f4f5" width="560px" />
<mj-wrapper padding="40px 16px" />
<mj-section padding="0" />
<mj-column padding="0" />
<mj-text padding="0 0 16px" font-size="15px" line-height="24px" color="#18181b" css-class="em-text" />
<mj-button align="left" background-color="#2f5bd3" color="#ffffff" font-size="15px" font-weight="600" line-height="20px" border-radius="8px" inner-padding="12px 22px" padding="8px 0 24px" css-class="em-button" />
<mj-divider border-width="1px" border-color="#e4e4e7" padding="12px 0 28px" css-class="em-rule" />
<mj-table padding="0 0 24px" font-size="14px" line-height="20px" color="#18181b" />
<mj-class name="card" background-color="#ffffff" border="1px solid #e4e4e7" border-radius="12px" padding="40px" css-class="em-card" />
<mj-class name="h1" font-size="24px" line-height="32px" font-weight="650" letter-spacing="-0.3px" css-class="em-text em-h1" />
<mj-class name="small" font-size="13px" line-height="20px" color="#52525b" padding="0 0 12px" css-class="em-muted" />
<mj-class name="danger" background-color="#b42318" css-class="em-button-danger" />
<mj-class name="footer" font-size="12px" line-height="18px" color="#52525b" align="center" padding="0 0 6px" css-class="em-muted" />
</mj-attributes>
<mj-style inline="inline">
.em-label { padding: 10px 0; color: #52525b; width: 45%; vertical-align: top; }
.em-value { padding: 10px 0; color: #18181b; text-align: right; vertical-align: top; }
.em-row-border { border-top: 1px solid #e4e4e7; }
.em-total { padding: 12px 0 10px; color: #18181b; font-weight: 650; border-top: 2px solid #18181b; vertical-align: top; }
.em-total-value { padding: 12px 0 10px; color: #18181b; font-weight: 650; border-top: 2px solid #18181b; text-align: right; vertical-align: top; }
.em-codebox { margin: 8px 0 8px; padding: 18px 12px; background-color: #f4f4f5; border: 1px solid #e4e4e7; border-radius: 8px; text-align: center; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace; font-size: 32px; line-height: 40px; font-weight: 700; letter-spacing: 8px; color: #18181b; }
.em-callout { padding: 14px 16px; background-color: #f4f4f5; border: 1px solid #e4e4e7; border-radius: 8px; font-size: 14px; line-height: 22px; color: #18181b; }
.em-track { height: 8px; font-size: 1px; line-height: 8px; border-radius: 999px; background-color: #f4f4f5; }
.em-fill { height: 8px; font-size: 1px; line-height: 8px; border-radius: 999px; background-color: #2f5bd3; }
.em-link { color: #2f5bd3; text-decoration: underline; }
.em-step { color: #52525b; }
</mj-style>
<mj-style>
body { -webkit-font-smoothing: antialiased; }
:root { color-scheme: light dark; supported-color-schemes: light dark; }
.em-logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
body, .em-page { background-color: #0e0e10 !important; }
.em-card, .em-card > table, .em-card > table > tbody > tr > td { background-color: #18181b !important; border-color: #2e2e33 !important; }
.em-text, .em-text > div { color: #f4f4f5 !important; }
.em-muted, .em-muted > div, .em-muted a { color: #a1a1aa !important; }
.em-link, .em-text a { color: #7a9bff !important; }
.em-wordmark, .em-wordmark > div, .em-wordmark a { color: #f4f4f5 !important; }
.em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
.em-border { border-color: #2e2e33 !important; }
.em-strong-border { border-color: #f4f4f5 !important; }
.em-rule, .em-rule p { border-top-color: #2e2e33 !important; }
.em-track { background-color: #232327 !important; }
.em-fill { background-color: #7a9bff !important; }
a.em-button, .em-button a, .em-button td[bgcolor] { background-color: #7a9bff !important; color: #0b0d14 !important; border-color: #7a9bff !important; }
a.em-button-danger, .em-button-danger a, .em-button-danger td[bgcolor] { background-color: #f97066 !important; color: #0b0d14 !important; border-color: #f97066 !important; }
.em-logo-light { display: none !important; }
.em-logo-dark { display: block !important; }
}
[data-ogsc] .em-card, [data-ogsc] .em-card > table, [data-ogsc] .em-card > table > tbody > tr > td { border-color: #2e2e33 !important; }
[data-ogsc] .em-text, [data-ogsc] .em-text > div { color: #f4f4f5 !important; }
[data-ogsc] .em-muted, [data-ogsc] .em-muted > div, [data-ogsc] .em-muted a { color: #a1a1aa !important; }
[data-ogsc] .em-link, [data-ogsc] .em-text a { color: #7a9bff !important; }
[data-ogsc] .em-wordmark, [data-ogsc] .em-wordmark > div, [data-ogsc] .em-wordmark a { color: #f4f4f5 !important; }
[data-ogsc] .em-subtle { border-color: #2e2e33 !important; }
[data-ogsc] .em-border { border-color: #2e2e33 !important; }
[data-ogsc] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsc] .em-rule, [data-ogsc] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsc] a.em-button, [data-ogsc] .em-button a, [data-ogsc] .em-button td[bgcolor] { color: #0b0d14 !important; border-color: #7a9bff !important; }
[data-ogsc] a.em-button-danger, [data-ogsc] .em-button-danger a, [data-ogsc] .em-button-danger td[bgcolor] { color: #0b0d14 !important; border-color: #f97066 !important; }
[data-ogsb] body, [data-ogsb] .em-page { background-color: #0e0e10 !important; }
[data-ogsb] .em-card, [data-ogsb] .em-card > table, [data-ogsb] .em-card > table > tbody > tr > td { background-color: #18181b !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-border { border-color: #2e2e33 !important; }
[data-ogsb] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsb] .em-rule, [data-ogsb] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsb] .em-track { background-color: #232327 !important; }
[data-ogsb] .em-fill { background-color: #7a9bff !important; }
[data-ogsb] a.em-button, [data-ogsb] .em-button a, [data-ogsb] .em-button td[bgcolor] { background-color: #7a9bff !important; border-color: #7a9bff !important; }
[data-ogsb] a.em-button-danger, [data-ogsb] .em-button-danger a, [data-ogsb] .em-button-danger td[bgcolor] { background-color: #f97066 !important; border-color: #f97066 !important; }
@media only screen and (max-width: 600px) {
.em-card > table > tbody > tr > td { padding: 32px 24px !important; }
.em-h1, .em-h1 > div { font-size: 22px !important; line-height: 30px !important; }
}
</mj-style>
<!-- theme:end -->
</mj-head>
<mj-body>
<!-- header:start -->
<!-- Generated from theme.ts by `npm run build`. Edit theme.ts, not this block. -->
<mj-wrapper css-class="em-page">
<mj-section padding="0 8px 24px">
<mj-column>
<mj-text padding="0" font-size="18px" line-height="28px" font-weight="700" letter-spacing="-0.2px" css-class="em-wordmark"><a href="https://example.com" style="color:#18181b;text-decoration:none;">Acme</a></mj-text>
</mj-column>
</mj-section>
<!-- header:end -->
<mj-section mj-class="card">
<mj-column>
<mj-text mj-class="h1">Your payment didn't go through</mj-text>
<mj-text>Hi {{name}}, we couldn't charge your {{payment_method}} for your {{plan_name}} plan. This usually happens when a card expires or the bank declines the charge.</mj-text>
<mj-table>
<tr><td class="em-label em-muted">Plan</td><td class="em-value em-text">{{plan_name}}</td></tr>
<tr><td class="em-label em-muted em-row-border em-border">Amount due</td><td class="em-value em-text em-row-border em-border">{{amount}}</td></tr>
<tr><td class="em-label em-muted em-row-border em-border">Next attempt</td><td class="em-value em-text em-row-border em-border">{{retry_date}}</td></tr>
</mj-table>
<mj-text>Update your payment method before {{retry_date}} to keep your account active.</mj-text>
<mj-button href="{{update_url}}">Update payment method</mj-button>
<mj-text mj-class="small" padding="0">Already updated your details? You can ignore this email.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 8px 0">
<mj-column>
<mj-text mj-class="footer">You're receiving this because a payment for your subscription failed.</mj-text>
</mj-column>
</mj-section>
<!-- footer:start -->
<!-- Generated from theme.ts by `npm run build`. Edit theme.ts, not this block. -->
<mj-section padding="0 8px">
<mj-column>
<mj-text mj-class="footer">Acme Inc. · 100 Market Street · San Francisco, CA 94105</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
<!-- footer:end -->
</mj-body>
</mjml>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title>
We couldn't charge {{payment_method}}. Update it before
{{retry_date}} to keep your plan.
</title>
<style>
:root { color-scheme: light dark; supported-color-schemes: light dark; }
.em-logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
.em-page, .em-page > table > tbody > tr > td { background-color: #0e0e10 !important; }
.em-card { background-color: #18181b !important; border-color: #2e2e33 !important; }
.em-text, .em-text > div { color: #f4f4f5 !important; }
.em-muted, .em-muted > div, .em-muted a { color: #a1a1aa !important; }
.em-link, .em-text a { color: #7a9bff !important; }
.em-wordmark, .em-wordmark > div, .em-wordmark a { color: #f4f4f5 !important; }
.em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
.em-border { border-color: #2e2e33 !important; }
.em-strong-border { border-color: #f4f4f5 !important; }
.em-rule, .em-rule p { border-top-color: #2e2e33 !important; }
.em-track { background-color: #232327 !important; }
.em-fill { background-color: #7a9bff !important; }
a.em-button, .em-button a, .em-button td[bgcolor] { background-color: #7a9bff !important; color: #0b0d14 !important; border-color: #7a9bff !important; }
a.em-button-danger, .em-button-danger a, .em-button-danger td[bgcolor] { background-color: #f97066 !important; color: #0b0d14 !important; border-color: #f97066 !important; }
.em-logo-light { display: none !important; }
.em-logo-dark { display: block !important; }
}
[data-ogsc] .em-card { border-color: #2e2e33 !important; }
[data-ogsc] .em-text, [data-ogsc] .em-text > div { color: #f4f4f5 !important; }
[data-ogsc] .em-muted, [data-ogsc] .em-muted > div, [data-ogsc] .em-muted a { color: #a1a1aa !important; }
[data-ogsc] .em-link, [data-ogsc] .em-text a { color: #7a9bff !important; }
[data-ogsc] .em-wordmark, [data-ogsc] .em-wordmark > div, [data-ogsc] .em-wordmark a { color: #f4f4f5 !important; }
[data-ogsc] .em-subtle { border-color: #2e2e33 !important; }
[data-ogsc] .em-border { border-color: #2e2e33 !important; }
[data-ogsc] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsc] .em-rule, [data-ogsc] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsc] a.em-button, [data-ogsc] .em-button a, [data-ogsc] .em-button td[bgcolor] { color: #0b0d14 !important; border-color: #7a9bff !important; }
[data-ogsc] a.em-button-danger, [data-ogsc] .em-button-danger a, [data-ogsc] .em-button-danger td[bgcolor] { color: #0b0d14 !important; border-color: #f97066 !important; }
[data-ogsb] .em-page, [data-ogsb] .em-page > table > tbody > tr > td { background-color: #0e0e10 !important; }
[data-ogsb] .em-card { background-color: #18181b !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-border { border-color: #2e2e33 !important; }
[data-ogsb] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsb] .em-rule, [data-ogsb] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsb] .em-track { background-color: #232327 !important; }
[data-ogsb] .em-fill { background-color: #7a9bff !important; }
[data-ogsb] a.em-button, [data-ogsb] .em-button a, [data-ogsb] .em-button td[bgcolor] { background-color: #7a9bff !important; border-color: #7a9bff !important; }
[data-ogsb] a.em-button-danger, [data-ogsb] .em-button-danger a, [data-ogsb] .em-button-danger td[bgcolor] { background-color: #f97066 !important; border-color: #f97066 !important; }
@media only screen and (max-width: 600px) {
.em-card-body > tbody > tr > td { padding: 32px 24px !important; }
.em-h1, .em-h1 > div { font-size: 22px !important; line-height: 30px !important; }
}
</style>
</head>
<body
class="em-page"
dir="ltr"
lang="en"
style="background-color:#f4f4f5;margin:0;padding:0">
<div
style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0"
data-skip-in-text="true">
We couldn't charge {{payment_method}}. Update it before
{{retry_date}} to keep your plan.
<div>
</div>
</div>
<table
border="0"
width="100%"
cellpadding="0"
cellspacing="0"
role="presentation"
align="center">
<tbody>
<tr>
<td
dir="ltr"
lang="en"
style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;-webkit-font-smoothing:antialiased">
<table
align="center"
width="100%"
class="em-page"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="max-width:560px;width:100%;margin:0 auto">
<tbody>
<tr style="width:100%">
<td style="padding:40px 16px">
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation">
<tbody>
<tr>
<td style="padding:0 8px 24px">
<a
href="https://example.com"
style="color:#067df7;text-decoration-line:none;text-decoration:none"
target="_blank"
><p
class="em-wordmark"
style="font-size:18px;line-height:28px;margin:0;font-weight:700;letter-spacing:-0.2px;color:#18181b;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">
Acme
</p></a
>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-card"
style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px">
<tbody>
<tr>
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-card-body">
<tbody>
<tr>
<td style="padding:40px 40px">
<h1
class="em-text em-h1"
style="margin:0 0 16px;font-size:24px;line-height:32px;font-weight:650;letter-spacing:-0.3px;color:#18181b">
Your payment didn't go through
</h1>
<p
class="em-text"
style="font-size:15px;line-height:24px;margin:0 0 16px;color:#18181b;margin-top:0;margin-right:0;margin-bottom:16px;margin-left:0">
Hi {{name}}, we couldn't charge your
{{payment_method}} for your {{plan_name}}
plan. This usually happens when a card
expires or the bank declines the charge.
</p>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin:0 0 24px">
<tbody>
<tr>
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-border">
<tbody style="width:100%">
<tr style="width:100%">
<td
class="em-muted"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#52525b;width:45%">
Plan
</td>
<td
class="em-text"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#18181b;text-align:right">
{{plan_name}}
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-border"
style="border-top:1px solid #e4e4e7">
<tbody style="width:100%">
<tr style="width:100%">
<td
class="em-muted"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#52525b;width:45%">
Amount due
</td>
<td
class="em-text"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#18181b;text-align:right">
{{amount}}
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-border"
style="border-top:1px solid #e4e4e7">
<tbody style="width:100%">
<tr style="width:100%">
<td
class="em-muted"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#52525b;width:45%">
Next attempt
</td>
<td
class="em-text"
data-id="__react-email-column"
style="padding:10px 0;font-size:14px;line-height:20px;vertical-align:top;color:#18181b;text-align:right">
{{retry_date}}
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p
class="em-text"
style="font-size:15px;line-height:24px;margin:0 0 16px;color:#18181b;margin-top:0;margin-right:0;margin-bottom:16px;margin-left:0">
Update your payment method before
{{retry_date}} to keep your account
active.
</p>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin:8px 0 24px">
<tbody>
<tr>
<td>
<a
class="em-button"
href="{{update_url}}"
style="line-height:20px;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#2f5bd3;color:#ffffff;font-size:15px;font-weight:600;border-radius:8px;padding:12px 22px;padding-top:12px;padding-right:22px;padding-bottom:12px;padding-left:22px"
target="_blank"
><span
><!--[if mso]><i style="mso-font-width:366.66666666666663%;mso-text-raise:18px" hidden>   </i><![endif]--></span
><span
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px"
>Update payment method</span
><span
><!--[if mso]><i style="mso-font-width:366.66666666666663%" hidden>   ​</i><![endif]--></span
></a
>
</td>
</tr>
</tbody>
</table>
<p
class="em-muted"
style="font-size:13px;line-height:20px;margin:0;color:#52525b;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">
Already updated your details? You can
ignore this email.
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="text-align:center">
<tbody>
<tr>
<td style="padding:24px 8px 0">
<p
class="em-muted"
style="font-size:12px;line-height:18px;margin:0 0 6px;color:#52525b;margin-top:0;margin-right:0;margin-bottom:6px;margin-left:0">
You're receiving this because a payment for
your subscription failed.
</p>
<p
class="em-muted"
style="font-size:12px;line-height:18px;margin:0 0 6px;color:#52525b;margin-top:0;margin-right:0;margin-bottom:6px;margin-left:0">
Acme Inc. · 100 Market Street · San Francisco, CA
94105
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
Your payment didn't go through
Hi {{name}}, we couldn't charge your {{payment_method}} for your {{plan_name}} plan. This usually happens when a card expires or the bank declines the charge.
Plan: {{plan_name}}
Amount due: {{amount}}
Next attempt: {{retry_date}}
Update your payment method before {{retry_date}} to keep your account active:
{{update_url}}
Already updated your details? You can ignore this email.
--
Acme
Acme Inc. · 100 Market Street · San Francisco, CA 94105
# Create the template in your account once
npx -y --package=groundmail-cli -- groundmail templates push templates/payment-failed/payment-failed.html \
--alias payment-failed \
--name "Payment failed" \
--subject "Your payment didn’t go through" \
--text templates/payment-failed/payment-failed.txt \
--publish
// Then send it by alias from your app
import { Groundmail } from 'groundmail'
const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)
await groundmail.emails.send({
from: 'Acme <noreply@yourdomain.com>',
to: 'ada@example.com',
template: {
id: 'payment-failed',
variables: {
name: 'Ada',
plan_name: 'Pro',
amount: '$54.13',
payment_method: 'Visa ending in 4242',
retry_date: 'October 19, 2026',
update_url: 'https://example.com/billing/payment-method',
},
},
})payment-failed.tsx, 71 lines, shown in full (scroll inside the panel). Typed props. Copy it with theme.ts and components/ from the repo. View on GitHub
Send it with Groundmail
Create the template in your account once, then send it by alias from your app.
npx -y --package=groundmail-cli -- groundmail templates push templates/payment-failed/payment-failed.html \
--alias payment-failed \
--name "Payment failed" \
--subject "Your payment didn’t go through" \
--text templates/payment-failed/payment-failed.txt \
--publishimport { Groundmail } from 'groundmail'
const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)
await groundmail.emails.send({
from: 'Acme <noreply@yourdomain.com>',
to: 'ada@example.com',
template: {
id: 'payment-failed',
variables: {
name: 'Ada',
plan_name: 'Pro',
amount: '$54.13',
payment_method: 'Visa ending in 4242',
retry_date: 'October 19, 2026',
update_url: 'https://example.com/billing/payment-method',
},
},
})The CLI command and the Node.js send from the template's README, which also has Python, PHP and cURL. View on GitHub
Subject lines
Say what happened, calmly. The template's default leaves the product out, so add it if your sender name doesn't make it obvious.
- Your payment didn’t go throughDefaultCalm and clear, without alarm.
- Your Acme payment didn’t go throughGoodAdds the product, for people with several subscriptions.
- Update your card to keep your Acme {{plan_name}} planGoodLeads with the fix, for a second reminder after the first.
- FINAL NOTICE: your account will be suspendedAvoidCapitals and threats read as a scam, and it isn't final when you retry on {{retry_date}}.
A failed-payment email in spam is a customer you lose
This is the email that keeps a paying customer. It has to reach the inbox before the retry date, and look nothing like the fakes that copy it.
SPF, DKIM and DMARC for transactional email →- Keep it on a transactional stream, apart from newsletters and marketing.
- Publish DMARC with an enforcing policy, so nobody else can send billing email as your domain.
- Link only to your own domain: no shorteners or redirects in a billing email.
- Groundmail sends this template by alias from your own domain, on our own mail servers. Send it with Groundmail
More billing templates
All email templates →- Receipt email templateA payment receipt with line items, tax, total and a link to the full invoice.
- Subscription renewal email templateAn upcoming-renewal reminder with the amount, date and card, sent before the charge.
- Usage alert email templateWarns before a plan limit is hit, with a progress bar and the reset date.
Questions
Is it free to use?
Yes. The templates are MIT-licensed: use them in commercial products and change them however you like.
Do I need Groundmail to use it?
No. The HTML and text files use plain placeholders that work with most providers' template engines and with Handlebars or Mustache. Sending through Groundmail just saves a step.
Does it work in dark mode and Outlook?
Yes. The full HTML file carries dark-mode styles for Apple Mail and Outlook.com, and the extra markup desktop Outlook needs to draw the button.
How do I change the colors and logo?
Edit theme.ts once (name, logo, address and colors for light and dark) and rebuild. Every template in the set picks it up.