Skip to content
Groundmail

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.

  1. What failed

    Which card, for which plan, and how much. People with several subscriptions need to know which one this is.

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

  3. When you'll try again

    The next attempt date tells people how long they have, and that nothing has been cancelled yet.

  4. One fix button

    Update the payment method. One button to the billing page, and nothing else to click.

  5. What happens next

    Say plainly what fixing it keeps: the template says "to keep your account active". No threats, no countdowns.

  6. 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.',
  )
}

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.

1Terminal
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
2Node.js
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',
    },
  },
})

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

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.