Invoice

An amount, a due date, a way to pay, and the PDF attached.

Get an API key

Billing · 9 placeholders

The job

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.

How this one works

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.

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

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.

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 invoice PDF

Generated before the send, because it is attached to this message rather than linked.

04

A billing contact

Often not the person who signed up. Ask for it, store it, and send here.

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
Invoice {{INVOICE_NUMBER}} from {{COMPANY_NAME}}, {{AMOUNT_DUE}} due {{DUE_DATE}}
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>

How it works

One send, start to finish. Every message is in your logs with its delivery events.

01

The invoice is issued

Your billing system writes the row and renders the PDF.

02

You send this

With the PDF attached as content_base64 and the amount in the subject.

03

They pay, or they do not

The pay link needs no account, because the person paying rarely has one.

04

You resend on a schedule

The same template, a reminder subject, and the same idempotency key discipline.

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> = {
  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);

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.

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.