Subscription notification emails: renewal, trial ending and failed payment

Updated

Subscription email is mostly about money and access, so accuracy matters more than copy. Each message should say what happened, what it costs, and what the customer needs to do, if anything.

The set most products need

| Event | Email | Timing | |---|---|---| | Subscription started | Welcome and receipt | Right away | | Trial ending | "Your trial ends on date" | A few days before | | Renewal upcoming | Heads-up for annual plans | A week or more before | | Payment succeeded | Receipt | Right away | | Payment failed | Update your card | At failure, then follow-ups | | Plan changed | Confirmation | Right away | | Cancelled | Confirmation and end date | Right away |

Workflow

Drive every email from your payment provider's webhook, not from your own UI. Record each provider event ID before sending so a redelivered webhook sends nothing twice.

import express from "express";
import { Inboxili, InboxiliError } from "@inboxili/node";

const inboxili = new Inboxili({ apiKey: process.env.INBOXILI_API_KEY });
const app = express();

app.post("/webhooks/billing", express.raw({ type: "application/json" }), async (req, res) => {
  const event = verifyProviderSignature(req); // your payment provider's SDK does this
  if (!(await db.events.insertIfNew(event.id))) return res.sendStatus(200); // already handled

  const sub = await db.subscriptions.find(event.data.subscriptionId);
  const user = await db.users.find(sub.userId);

  const template = {
    "invoice.payment_failed": {
      subject: "Your payment did not go through",
      html: '<p>Hi {{name}}, we could not charge your card for {{plan}}. <a href="{{url}}">Update your payment method</a> to keep your access.</p>',
    },
    "customer.subscription.trial_will_end": {
      subject: "Your trial ends on {{date}}",
      html: '<p>Hi {{name}}, your {{plan}} trial ends on {{date}}. <a href="{{url}}">Choose a plan</a> to keep going.</p>',
    },
  }[event.type];
  if (!template) return res.sendStatus(200);

  try {
    await inboxili.emails.send({
      to: user.email,
      fromEmail: "billing@yourdomain.com",
      fromName: "Acme Billing",
      subject: template.subject,
      htmlBody: template.html,
      templateData: { name: user.firstName, plan: sub.planName, date: sub.trialEnd?.toISOString().slice(0, 10) ?? "", url: "https://app.yourdomain.com/billing" },
    });
  } catch (err) {
    if (!(err instanceof InboxiliError)) throw err;
    console.error("billing email failed", err.status, err.code);
  }
  res.sendStatus(200);
});

verifyProviderSignature and db stand in for your payment provider's verification helper and your database. Return 200 to the provider even if the email fails, and log the failure, so a flaky email call does not trigger endless webhook retries.

Things that go wrong

  • Duplicate emails from webhook redelivery. The event ID table above is the fix.
  • Trial-ending emails. Providers differ on whether they send a trial-ending event. If yours does not, schedule it yourself from the stored trial end date.
  • Wrong amount or currency. Format from integer minor units and the currency code.
  • Failed-payment email with no link. Always link straight to the billing page.
  • Tone. Failed payments are usually expired cards, not misbehaviour. Say so.

Delivery events

If a failed-payment email bounces, the customer will lose access without knowing why. Flag the account and show an in-app banner as well.

Deliverability notes

Send billing mail from a dedicated billing@ address on your verified domain and keep it free of promotions.

Frequently asked questions

How many failed-payment emails should I send?
Enough to recover the customer without nagging: commonly one at the first failure, one a few days later, and a final notice before access changes. Match the retry schedule configured in your payment provider.
Are these transactional or marketing?
Billing notices about the customer's own subscription are generally transactional. A message that tries to upsell them is not, so send upsells as campaigns.

Official SDKs

Send your first billing email

Create a workspace, verify a domain, and make your first API call.

Related