Order shipped
The tracking number, the carrier, and when it should arrive.
Commerce · 9 placeholders
The job
The shipping email is opened more than any other email a store sends, and it is opened for one number. Put the tracking number where it can be read without scrolling, and make it selectable text rather than an image or a button label.
How this one works
The delivery address is here for a reason. A person who has moved sees the old address the day the parcel leaves rather than the week it comes back, and that is a support ticket instead of a refund.
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 carrier accepts the parcel and a tracking number exists. Not when the label is printed, which is often days earlier and makes the tracking page look broken.
A sending domain
Verified with three DNS records, or send from onboarding@emails.sh until yours is ready.
A tracking number from the carrier
The one that resolves on their site. Send when it resolves, not when the label prints.
The address as shipped
Copied from the label, so the email and the parcel cannot disagree.
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.
Your {{STORE_NAME}} order {{ORDER_NUMBER}} has shipped<!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>Your order has shipped</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;">{{CARRIER}}, arriving {{ESTIMATED_DELIVERY}}.            </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;">Order {{ORDER_NUMBER}} is on its way</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;">{{FIRST_NAME}}, your order left us today with {{CARRIER}}.</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%">Carrier</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;">{{CARRIER}}</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%">Tracking number</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;">{{TRACKING_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%">Estimated delivery</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;">{{ESTIMATED_DELIVERY}}</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%">Shipping to</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;">{{SHIPPING_ADDRESS}}</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="{{TRACKING_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;">Track this parcel</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="{{TRACKING_URL}}" style="color:#3f3f46;text-decoration:underline;word-break:break-all;">{{TRACKING_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;">Carrier tracking can take a few hours to show the first scan. If the address above is wrong, write to <a href="mailto:{{SUPPORT_EMAIL}}" style="color:#18181b;text-decoration:underline;">{{SUPPORT_EMAIL}}</a> today and quote order {{ORDER_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;">{{STORE_NAME}} sent this about order {{ORDER_NUMBER}}.</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 carrier takes the parcel
A tracking number exists and resolves on their site.
You send this
Keyed on the order and the tracking number, so a re-scan sends nothing twice.
They check the number
From the email, usually within the hour, often more than once.
A split shipment repeats it
One email per parcel, each with its own tracking number and its own key.
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> = {
STORE_NAME: 'Northwind Supply',
FIRST_NAME: 'Ada',
ORDER_NUMBER: '10428',
CARRIER: 'DHL Express',
TRACKING_NUMBER: 'JD0140021234567890',
TRACKING_URL: 'https://www.dhl.com/track?id=JD0140021234567890',
ESTIMATED_DELIVERY: 'Tuesday 4 August',
SHIPPING_ADDRESS: '12 Bishopsgate, London EC2N 4AJ',
SUPPORT_EMAIL: 'help@northwind.com',
};
const fill = (body: string) =>
Object.entries(vars).reduce(
(out, [name, value]) => out.replaceAll('{{' + name + '}}', value),
body
);
const { id } = await mail.send({
from: 'Northwind Supply <onboarding@emails.sh>',
to: ['ada@example.com'],
subject: fill('Your {{STORE_NAME}} order {{ORDER_NUMBER}} has shipped'),
html: fill(readFileSync('email.html', 'utf8')),
text: fill(readFileSync('email.txt', 'utf8')),
idempotencyKey: 'shipped-' + vars.ORDER_NUMBER + '-' + vars.TRACKING_NUMBER,
tags: { type: 'order-shipped' }
});
// Queued, not delivered. Ask for the id later, or take the webhook.
console.log(id);Questions
Should the tracking number be a link or text?
Both, in that order: the number as selectable text in the table, the carrier page as the button. People paste the number into the carrier app more often than they follow the link.
What about multiple parcels on one order?
One email per parcel, each saying which items are in it. A single email listing three tracking numbers gets read as one, and the last two parcels are reported missing.
Can we add the delivered email later?
Yes, and it should be a separate template. Reusing this one with a changed subject leaves an estimated delivery date in an email sent after delivery.
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.