Receipt

What was charged, to what card, with a line the reader can expense.

Get an API key

Billing · 11 placeholders

The job

A receipt is read twice: once on arrival, and once six weeks later by somebody searching their inbox for the amount. It has to make sense both times, which means the number, the date, and the last four digits are the content and everything else is packaging.

How this one works

It is also the email most likely to be forwarded to an accountant, so the plain-text version matters more here than anywhere else. Half of accounting software strips HTML before a human sees it.

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 a successful charge, driven by your payment provider webhook rather than by the checkout response, so a customer who closed the tab still gets it.

01

An emails.sh API key

Created in the dashboard, starting with esh_. It stays on your server.

02

A sending domain

Verified with three DNS records, or send from onboarding@emails.sh until yours is ready.

03

The charge, after it settled

Amount, currency, date, and the last four digits, read from the payment webhook.

04

An idempotency key

Webhooks retry. The receipt number makes a repeat delivery a no-op instead of a second email.

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.

Subject
Your {{PRODUCT_NAME}} receipt {{RECEIPT_NUMBER}} for {{TOTAL_AMOUNT}}
HTML
<!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>Receipt</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;">{{TOTAL_AMOUNT}} paid on {{PAID_DATE}}.&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;&#8199;&#65279;</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;">Receipt from {{PRODUCT_NAME}}</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;">Thanks. Your payment of {{TOTAL_AMOUNT}} went through on {{PAID_DATE}}.</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%">Receipt</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;">{{RECEIPT_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%">{{ITEM_NAME}}</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;">{{ITEM_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%">Tax</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;">{{TAX_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%">Total paid</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%">Paid with</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;">{{CARD_BRAND}} ending {{CARD_LAST4}}</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="{{RECEIPT_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 or download this receipt</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="{{RECEIPT_URL}}" style="color:#3f3f46;text-decoration:underline;word-break:break-all;">{{RECEIPT_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;">Something wrong with this charge? Reply, or write to <a href="mailto:{{BILLING_EMAIL}}" style="color:#18181b;text-decoration:underline;">{{BILLING_EMAIL}}</a>, and quote {{RECEIPT_NUMBER}}.</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 payment.</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.

01

The charge settles

Your payment provider sends the webhook. That is the trigger, not the checkout page.

02

You write the receipt row

With its own number, before any mail is sent.

03

You send this

With idempotency_key set to that number, so a retried webhook sends nothing twice.

04

They find it later

The subject carries the number and the amount, which is what people search for.

Code

The same send three ways. Each one fills the placeholders, reads the two bodies, and posts them to POST /v1/emails.

npm install @emails.sh/sdk
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',
  RECEIPT_NUMBER: 'RCPT-2026-0418',
  PAID_DATE: '31 July 2026',
  ITEM_NAME: 'Team plan, 5 seats',
  ITEM_AMOUNT: '$120.00',
  TAX_AMOUNT: '$28.80',
  TOTAL_AMOUNT: '$148.80',
  CARD_BRAND: 'Visa',
  CARD_LAST4: '4242',
  RECEIPT_URL: 'https://acme.com/receipts/RCPT-2026-0418',
  BILLING_EMAIL: 'billing@acme.com',
};

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('Your {{PRODUCT_NAME}} receipt {{RECEIPT_NUMBER}} for {{TOTAL_AMOUNT}}'),
  html: fill(readFileSync('email.html', 'utf8')),
  text: fill(readFileSync('email.txt', 'utf8')),
  idempotencyKey: 'receipt-' + vars.RECEIPT_NUMBER,
  tags: { type: 'receipt' }
});

// Queued, not delivered. Ask for the id later, or take the webhook.
console.log(id);

Questions

Why put the amount in the subject line?

Because the search that finds this email six weeks later is usually the amount or the vendor name. Both are in the subject, so the mail is findable without opening anything.

Should the PDF be attached?

Link it rather than attach it for a normal receipt: attachments make the message bigger, and some filters treat a PDF from an unknown sender harshly. Attach it when a customer or a jurisdiction requires the document itself, which is what the invoice template does.

What stops a retried webhook sending two receipts?

idempotency_key. The same key inside the retention window returns the first message id instead of sending again, so at-least-once delivery from your payment provider does not become two emails.

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.