# Invoice

An invoice email template with the amount due, payment terms, a pay link, and the PDF attached. Table-based HTML for Outlook, a text alternative, and code in TypeScript, Python, and curl.

An invoice email is usually read by somebody who did not buy anything: accounts payable, working through a queue. They need the number, the amount, the due date, and the document, in that order, and they will not go hunting for any of them.

This is the one transactional email where the attachment is not optional. Accounts payable systems ingest the PDF, and a link to a hosted invoice behind a login is where a payment goes to die.

Topics: invoice, accounts payable, billing, payment terms.

## When it sends

When the invoice is issued, and again as a reminder on your own schedule. The reminder is the same template with a different subject line and a due date that has moved into the past.

## 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 invoice PDF**: Generated before the send, because it is attached to this message rather than linked.
- **A billing contact**: Often not the person who signed up. Ask for it, store it, and send here.

## Subject

```text
Invoice {{INVOICE_NUMBER}} from {{COMPANY_NAME}}, {{AMOUNT_DUE}} due {{DUE_DATE}}
```

## 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>Invoice</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;">{{AMOUNT_DUE}} due {{DUE_DATE}}. PDF attached.&#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;">Invoice {{INVOICE_NUMBER}}</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;">{{COMPANY_NAME}} has issued an invoice to {{CUSTOMER_NAME}}. The PDF is attached.</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%">Issued</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;">{{ISSUE_DATE}}</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%">Terms</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;">{{PAYMENT_TERMS}}</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%">Due</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;">{{DUE_DATE}}</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%">Amount due</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;">{{AMOUNT_DUE}}</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="{{PAY_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;">Pay this 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="{{PAY_URL}}" style="color:#3f3f46;text-decoration:underline;word-break:break-all;">{{PAY_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;">Paying by transfer instead, or need a purchase order number on it? Reply, or write to <a href="mailto:{{BILLING_EMAIL}}" style="color:#18181b;text-decoration:underline;">{{BILLING_EMAIL}}</a> quoting {{INVOICE_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;">{{COMPANY_NAME}} sent this invoice to the billing contact for {{CUSTOMER_NAME}}.</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
```

## Plain text body

```text
Invoice {{INVOICE_NUMBER}}

{{COMPANY_NAME}} has issued an invoice to {{CUSTOMER_NAME}}. The PDF is
attached to this email.

Invoice:     {{INVOICE_NUMBER}}
Issued:      {{ISSUE_DATE}}
Terms:       {{PAYMENT_TERMS}}
Due:         {{DUE_DATE}}
Amount due:  {{AMOUNT_DUE}}

Pay online:
{{PAY_URL}}

Paying by transfer instead, or need a purchase order number on it? Reply,
or write to {{BILLING_EMAIL}} quoting {{INVOICE_NUMBER}}.
```

## Placeholders

| Placeholder | What it is | Example |
| --- | --- | --- |
| {{COMPANY_NAME}} | Who is invoicing. | Acme Inc |
| {{CUSTOMER_NAME}} | Who is being invoiced. | Northwind Ltd |
| {{INVOICE_NUMBER}} | Your invoice reference. | INV-2041 |
| {{ISSUE_DATE}} | When it was issued. | 31 July 2026 |
| {{DUE_DATE}} | When payment is due, spelled out. | 30 August 2026 |
| {{PAYMENT_TERMS}} | The terms in words. | Net 30 |
| {{AMOUNT_DUE}} | The total owed, with currency. | $4,800.00 |
| {{PAY_URL}} | A payment page that needs no login. | https://acme.com/pay/INV-2041 |
| {{BILLING_EMAIL}} | Where a query about this invoice goes. | ar@acme.com |

## How it works

1. **The invoice is issued** Your billing system writes the row and renders the PDF.
2. **You send this** With the PDF attached as content_base64 and the amount in the subject.
3. **They pay, or they do not** The pay link needs no account, because the person paying rarely has one.
4. **You resend on a schedule** The same template, a reminder subject, and the same idempotency key discipline.

## 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> = {
  COMPANY_NAME: 'Acme Inc',
  CUSTOMER_NAME: 'Northwind Ltd',
  INVOICE_NUMBER: 'INV-2041',
  ISSUE_DATE: '31 July 2026',
  DUE_DATE: '30 August 2026',
  PAYMENT_TERMS: 'Net 30',
  AMOUNT_DUE: '$4,800.00',
  PAY_URL: 'https://acme.com/pay/INV-2041',
  BILLING_EMAIL: 'ar@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 Inc <onboarding@emails.sh>',
  to: ['ada@example.com'],
  subject: fill('Invoice {{INVOICE_NUMBER}} from {{COMPANY_NAME}}, {{AMOUNT_DUE}} due {{DUE_DATE}}'),
  html: fill(readFileSync('email.html', 'utf8')),
  text: fill(readFileSync('email.txt', 'utf8')),
  // The PDF, not a link to it: accounts payable ingests the document.
  attachments: [{ filename: 'invoice.pdf', content: readFileSync('invoice.pdf') }],
  idempotencyKey: 'invoice-' + vars.INVOICE_NUMBER
});

// 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 = {
    "COMPANY_NAME": "Acme Inc",
    "CUSTOMER_NAME": "Northwind Ltd",
    "INVOICE_NUMBER": "INV-2041",
    "ISSUE_DATE": "31 July 2026",
    "DUE_DATE": "30 August 2026",
    "PAYMENT_TERMS": "Net 30",
    "AMOUNT_DUE": "$4,800.00",
    "PAY_URL": "https://acme.com/pay/INV-2041",
    "BILLING_EMAIL": "ar@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 Inc <onboarding@emails.sh>",
    to=["ada@example.com"],
    subject=fill("Invoice {{INVOICE_NUMBER}} from {{COMPANY_NAME}}, {{AMOUNT_DUE}} due {{DUE_DATE}}"),
    html=fill(html),
    text=fill(text),
    # The PDF, not a link to it: accounts payable ingests the document.
    attachments=[{"filename": "invoice.pdf", "content": open("invoice.pdf", "rb").read()}],
    idempotency_key="invoice-" + values["INVOICE_NUMBER"],
)

# 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|{{COMPANY_NAME}}|Acme Inc|g' \
      -e 's|{{CUSTOMER_NAME}}|Northwind Ltd|g' \
      -e 's|{{INVOICE_NUMBER}}|INV-2041|g' \
      -e 's|{{ISSUE_DATE}}|31 July 2026|g' \
      -e 's|{{DUE_DATE}}|30 August 2026|g' \
      -e 's|{{PAYMENT_TERMS}}|Net 30|g' \
      -e 's|{{AMOUNT_DUE}}|$4,800.00|g' \
      -e 's|{{PAY_URL}}|https://acme.com/pay/INV-2041|g' \
      -e 's|{{BILLING_EMAIL}}|ar@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 'Invoice INV-2041 from Acme Inc, $4,800.00 due 30 August 2026' \
      --arg html "$(fill email.html)" \
      --arg text "$(fill email.txt)" \
      --arg pdf "$(base64 < invoice.pdf | tr -d '\n')" \
      '{from: "Acme Inc <onboarding@emails.sh>",
        to: ["ada@example.com"],
        subject: $subject,
        html: $html,
        text: $text,
        attachments: [{filename: "invoice.pdf", content_base64: $pdf}],
        idempotency_key: "invoice-INV-2041"}')"
```

## Questions

### How large can the attachment be?

Keep the whole message under 40 MB after base64, which inflates a file by about a third, so a 30 MB file is already 40 MB encoded. An invoice PDF is tens of kilobytes, so the only way to hit that ceiling is by attaching scans nobody asked for.

### Why does the pay link not require a login?

Because accounts payable is not your user. A payment page keyed to the invoice number and a token in the URL gets paid; a login wall gets an email asking for the invoice again.

### Should reminders be a different template?

No. Same body, different subject, and a line about the due date having passed. Two templates drift, and then the reminder quotes a total the invoice does not.

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