Subscription receipt
The renewal charge, the period it covers, and the next one.
Billing · 10 placeholders
The job
A renewal receipt is the same charge, every month, to somebody who has stopped thinking about it. That is exactly why it must be sent: the alternative is a customer who notices the line on a statement instead and calls it fraud.
How this one works
What separates this from a one-off receipt is the period. Say which dates the money bought and when the next charge lands, because a subscriber who can see the next date is a subscriber who cancels in your dashboard rather than at their bank.
What you need
A key, a domain, and the two or three things only your app knows. Nothing here takes longer than the email did to read.
When it sends
On each successful renewal charge, from the payment provider webhook. A failed renewal is a different email, and it should not look like this one.
A sending domain
Verified with three DNS records, or send from onboarding@emails.sh until yours is ready.
The renewal event
The invoice paid webhook, with the period it covers and the amount that settled.
The next charge date
The date your billing system will act on. Naming it prevents most billing tickets.
The email
Table layout, inline styles, no stylesheet, no web font, no JavaScript. It renders the same in Outlook, Gmail, and Apple Mail, and the plain-text version goes with it every time.
{{PRODUCT_NAME}} subscription renewed, {{TOTAL_AMOUNT}}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<!-- Stops iOS Mail resizing the text on its own. -->
<meta name="x-apple-disable-message-reformatting">
<title>Subscription renewed</title>
</head>
<body style="margin:0;padding:0;width:100%;background-color:#f4f4f5;">
<div style="display:none;max-height:0;max-width:0;opacity:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#f4f4f5;">{{PLAN_NAME}} for {{PERIOD_START}} to {{PERIOD_END}}.            </div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f4f4f5;">
<tr><td align="center" style="padding:24px 12px;">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:100%;background-color:#ffffff;border:1px solid #e4e4e7;">
<tr><td style="padding:32px;">
<h1 style="margin:0 0 16px;font-family:Helvetica,Arial,sans-serif;font-size:21px;line-height:28px;font-weight:600;color:#18181b;">Your subscription renewed</h1>
<p style="margin:0 0 16px;font-family:Helvetica,Arial,sans-serif;font-size:15px;line-height:24px;mso-line-height-rule:exactly;color:#3f3f46;">{{TOTAL_AMOUNT}} was charged to the card ending {{CARD_LAST4}} for {{PLAN_NAME}}, covering {{PERIOD_START}} to {{PERIOD_END}}.</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 20px;border-top:1px solid #e4e4e7;"><tr><td style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#71717a;" width="45%">Invoice</td><td align="right" style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#18181b;font-weight:600;">{{INVOICE_NUMBER}}</td></tr><tr><td style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#71717a;" width="45%">Plan</td><td align="right" style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#18181b;font-weight:600;">{{PLAN_NAME}}</td></tr><tr><td style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#71717a;" width="45%">Period</td><td align="right" style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#18181b;font-weight:600;">{{PERIOD_START}} to {{PERIOD_END}}</td></tr><tr><td style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#71717a;" width="45%">Charged</td><td align="right" style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#18181b;font-weight:600;">{{TOTAL_AMOUNT}}</td></tr><tr><td style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#71717a;" width="45%">Next charge</td><td align="right" style="padding:10px 0;border-bottom:1px solid #e4e4e7;font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#18181b;font-weight:600;">{{NEXT_CHARGE_DATE}}</td></tr></table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 20px;"><tr><td bgcolor="#18181b" style="background-color:#18181b;"><a href="{{INVOICE_URL}}" style="display:inline-block;padding:13px 24px;font-family:Helvetica,Arial,sans-serif;font-size:15px;line-height:20px;font-weight:600;color:#ffffff;text-decoration:none;">View invoice</a></td></tr></table>
<p style="margin:0 0 12px;font-family:Helvetica,Arial,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#71717a;">Button not working? Paste this into your browser:<br><a href="{{INVOICE_URL}}" style="color:#3f3f46;text-decoration:underline;word-break:break-all;">{{INVOICE_URL}}</a></p>
<p style="margin:0 0 12px;font-family:Helvetica,Arial,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#71717a;">Change plan, update the card, or cancel before {{NEXT_CHARGE_DATE}}: <a href="{{BILLING_URL}}" style="color:#18181b;text-decoration:underline;">{{BILLING_URL}}</a>.</p>
</td></tr>
</table>
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:100%;">
<tr><td style="padding:20px 32px 0;">
<p style="margin:0 0 8px;font-family:Helvetica,Arial,sans-serif;font-size:12px;line-height:18px;color:#71717a;">{{PRODUCT_NAME}} sends a receipt for every renewal.</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>How it works
One send, start to finish. Every message is in your logs with its delivery events.
The renewal charge settles
The invoice paid webhook arrives with the period and the amount.
You send this
Keyed on the invoice number, so a retried webhook cannot duplicate it.
They see the next date
Which is the number that stops a cancellation becoming a chargeback.
A failure takes another path
A declined renewal gets its own email that says what to fix and by when.
Code
The same send three ways. Each one fills the placeholders, reads the two bodies, and posts them to POST /v1/emails.
import { readFileSync } from 'node:fs';
import { Emailssh } from '@emails.sh/sdk';
const mail = new Emailssh({ apiKey: process.env.EMAILSSH_API_KEY });
// What changes per send. In your app these come from the row you just
// wrote, not from literals.
const vars: Record<string, string> = {
PRODUCT_NAME: 'Acme',
PLAN_NAME: 'Team, 5 seats',
TOTAL_AMOUNT: '$120.00',
PERIOD_START: '1 August 2026',
PERIOD_END: '31 August 2026',
NEXT_CHARGE_DATE: '1 September 2026',
CARD_LAST4: '4242',
INVOICE_NUMBER: 'INV-2026-08-0031',
INVOICE_URL: 'https://acme.com/invoices/INV-2026-08-0031',
BILLING_URL: 'https://acme.com/settings/billing',
};
const fill = (body: string) =>
Object.entries(vars).reduce(
(out, [name, value]) => out.replaceAll('{{' + name + '}}', value),
body
);
const { id } = await mail.send({
from: 'Acme <onboarding@emails.sh>',
to: ['ada@example.com'],
subject: fill('{{PRODUCT_NAME}} subscription renewed, {{TOTAL_AMOUNT}}'),
html: fill(readFileSync('email.html', 'utf8')),
text: fill(readFileSync('email.txt', 'utf8')),
idempotencyKey: 'renewal-' + vars.INVOICE_NUMBER,
tags: { type: 'subscription-receipt' }
});
// Queued, not delivered. Ask for the id later, or take the webhook.
console.log(id);Questions
Should a failed renewal use this template?
No. A dunning email has a different job: it says what failed, what will be retried, when access stops, and where to fix the card. Reusing the receipt for it reads as though the money went through.
Monthly receipts feel like clutter. Can we skip them?
Skipping them moves the discovery of the charge to a bank statement, which is the most expensive place a customer can find it. It is also the sort of thing card networks look at when a dispute rate climbs.
Where do tax details go?
On the invoice document behind the link, which is where a finance team looks anyway. Keep the email to the summary and let the hosted copy carry the full breakdown.
Send this one in the next five minutes.
Before your domain is verified you can send from onboarding@emails.sh, so the first send needs a key and nothing else. Verification is three DNS records and one screen.
Every message has delivery events you can read: accepted, delivered, bounced, complained. No support ticket to find out whether it arrived.
More billing templates