# Receipt

A payment receipt email template with line items, a total, the card used, and a link to the hosted receipt. Table-based HTML, a text alternative, an idempotency key, and code in TypeScript, Python, and curl.

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.

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.

Topics: receipt, payment, stripe, billing email.

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

## What you need

- **An emails.sh API key**: Created in the dashboard, starting with esh_. It stays on your server.
- **A sending domain**: Verified with three DNS records, or send from onboarding@emails.sh until yours is ready.
- **The charge, after it settled**: Amount, currency, date, and the last four digits, read from the payment webhook.
- **An idempotency key**: Webhooks retry. The receipt number makes a repeat delivery a no-op instead of a second email.

## Subject

```text
Your {{PRODUCT_NAME}} receipt {{RECEIPT_NUMBER}} for {{TOTAL_AMOUNT}}
```

## HTML body

```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>
```

## Plain text body

```text
Receipt from {{PRODUCT_NAME}}

Your payment of {{TOTAL_AMOUNT}} went through on {{PAID_DATE}}.

Receipt:      {{RECEIPT_NUMBER}}
{{ITEM_NAME}}: {{ITEM_AMOUNT}}
Tax:          {{TAX_AMOUNT}}
Total paid:   {{TOTAL_AMOUNT}}
Paid with:    {{CARD_BRAND}} ending {{CARD_LAST4}}

View or download this receipt:
{{RECEIPT_URL}}

Something wrong with this charge? Reply, or write to {{BILLING_EMAIL}},
and quote {{RECEIPT_NUMBER}}.
```

## Placeholders

| Placeholder | What it is | Example |
| --- | --- | --- |
| {{PRODUCT_NAME}} | Who charged them. | Acme |
| {{RECEIPT_NUMBER}} | Your reference for this payment. Also the idempotency key. | RCPT-2026-0418 |
| {{PAID_DATE}} | When the charge settled. | 31 July 2026 |
| {{ITEM_NAME}} | What was bought. | Team plan, 5 seats |
| {{ITEM_AMOUNT}} | The line amount, with the currency symbol. | $120.00 |
| {{TAX_AMOUNT}} | Tax charged, or 0 in the same format. | $28.80 |
| {{TOTAL_AMOUNT}} | What actually left the card. | $148.80 |
| {{CARD_BRAND}} | Card brand, as the reader would recognise it. | Visa |
| {{CARD_LAST4}} | Last four digits. Never more than four. | 4242 |
| {{RECEIPT_URL}} | The hosted copy, for a reader who needs a PDF. | https://acme.com/receipts/RCPT-2026-0418 |
| {{BILLING_EMAIL}} | Where billing questions go. | billing@acme.com |

## How it works

1. **The charge settles** Your payment provider sends the webhook. That is the trigger, not the checkout page.
2. **You write the receipt row** With its own number, before any mail is sent.
3. **You send this** With idempotency_key set to that number, so a retried webhook sends nothing twice.
4. **They find it later** The subject carries the number and the amount, which is what people search for.

## Code

### TypeScript

`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);
```

### Python

`pip install emailssh`

```
import os

from emailssh import Emailssh

mail = Emailssh(api_key=os.environ["EMAILSSH_API_KEY"])

# What changes per send. In your app these come from the row you just
# wrote, not from literals.
values = {
    "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",
}


def fill(body: str) -> str:
    for name, value in values.items():
        body = body.replace("{{" + name + "}}", value)
    return body


with open("email.html", encoding="utf-8") as f:
    html = f.read()
with open("email.txt", encoding="utf-8") as f:
    text = f.read()

sent = mail.send(
    # from_ rather than from, which is a Python keyword.
    from_="Acme <onboarding@emails.sh>",
    to=["ada@example.com"],
    subject=fill("Your {{PRODUCT_NAME}} receipt {{RECEIPT_NUMBER}} for {{TOTAL_AMOUNT}}"),
    html=fill(html),
    text=fill(text),
    idempotency_key="receipt-" + values["RECEIPT_NUMBER"],
    tags={"type": "receipt"},
)

# Queued, not delivered. Ask for the id later, or take the webhook.
print(sent["id"])
```

### curl

`Anywhere with curl and jq`

```
export EMAILSSH_API_KEY=esh_live_...

# The API substitutes nothing, so fill the placeholders before you post.
fill() {
  sed \
      -e 's|{{PRODUCT_NAME}}|Acme|g' \
      -e 's|{{RECEIPT_NUMBER}}|RCPT-2026-0418|g' \
      -e 's|{{PAID_DATE}}|31 July 2026|g' \
      -e 's|{{ITEM_NAME}}|Team plan, 5 seats|g' \
      -e 's|{{ITEM_AMOUNT}}|$120.00|g' \
      -e 's|{{TAX_AMOUNT}}|$28.80|g' \
      -e 's|{{TOTAL_AMOUNT}}|$148.80|g' \
      -e 's|{{CARD_BRAND}}|Visa|g' \
      -e 's|{{CARD_LAST4}}|4242|g' \
      -e 's|{{RECEIPT_URL}}|https://acme.com/receipts/RCPT-2026-0418|g' \
      -e 's|{{BILLING_EMAIL}}|billing@acme.com|g' "$1"
}

curl -sS https://emails.sh/v1/emails \
  -H "Authorization: Bearer $EMAILSSH_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
      --arg subject 'Your Acme receipt RCPT-2026-0418 for $148.80' \
      --arg html "$(fill email.html)" \
      --arg text "$(fill email.txt)" \
      '{from: "Acme <onboarding@emails.sh>",
        to: ["ada@example.com"],
        subject: $subject,
        html: $html,
        text: $text,
        idempotency_key: "receipt-RCPT-2026-0418",
        tags: {type: "receipt"}}')"
```

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

Docs: https://emails.sh/docs.md
All templates: https://emails.sh/templates.md