# Order shipped

An order shipped email template with the carrier, tracking number, delivery estimate, and address. Table-based HTML for Outlook, a text alternative, and code in TypeScript, Python, and curl.

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.

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.

Topics: order shipped, tracking, ecommerce, delivery.

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

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

## Subject

```text
Your {{STORE_NAME}} order {{ORDER_NUMBER}} has shipped
```

## 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>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}}.&#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;">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>
```

## Plain text body

```text
Order {{ORDER_NUMBER}} is on its way

{{FIRST_NAME}}, your order left us today with {{CARRIER}}.

Carrier:             {{CARRIER}}
Tracking number:     {{TRACKING_NUMBER}}
Estimated delivery:  {{ESTIMATED_DELIVERY}}
Shipping to:         {{SHIPPING_ADDRESS}}

Track this parcel:
{{TRACKING_URL}}

Carrier tracking can take a few hours to show the first scan. If the
address above is wrong, write to {{SUPPORT_EMAIL}} today and quote order
{{ORDER_NUMBER}}.
```

## Placeholders

| Placeholder | What it is | Example |
| --- | --- | --- |
| {{STORE_NAME}} | The shop, as the buyer knows it. | Northwind Supply |
| {{FIRST_NAME}} | What to call them. | Ada |
| {{ORDER_NUMBER}} | Their order reference. | 10428 |
| {{CARRIER}} | Who is carrying it. | DHL Express |
| {{TRACKING_NUMBER}} | The number, as selectable text. | JD0140021234567890 |
| {{TRACKING_URL}} | The carrier page for that number. | https://www.dhl.com/track?id=JD0140021234567890 |
| {{ESTIMATED_DELIVERY}} | The window the carrier gave. | Tuesday 4 August |
| {{SHIPPING_ADDRESS}} | Where it is going, as printed on the label. | 12 Bishopsgate, London EC2N 4AJ |
| {{SUPPORT_EMAIL}} | Where a delivery problem goes. | help@northwind.com |

## How it works

1. **The carrier takes the parcel** A tracking number exists and resolves on their site.
2. **You send this** Keyed on the order and the tracking number, so a re-scan sends nothing twice.
3. **They check the number** From the email, usually within the hour, often more than once.
4. **A split shipment repeats it** One email per parcel, each with its own tracking number and its own key.

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

### 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 = {
    "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",
}


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_="Northwind Supply <onboarding@emails.sh>",
    to=["ada@example.com"],
    subject=fill("Your {{STORE_NAME}} order {{ORDER_NUMBER}} has shipped"),
    html=fill(html),
    text=fill(text),
    idempotency_key="shipped-" + values["ORDER_NUMBER"] + "-" + values["TRACKING_NUMBER"],
    tags={"type": "order-shipped"},
)

# 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|{{STORE_NAME}}|Northwind Supply|g' \
      -e 's|{{FIRST_NAME}}|Ada|g' \
      -e 's|{{ORDER_NUMBER}}|10428|g' \
      -e 's|{{CARRIER}}|DHL Express|g' \
      -e 's|{{TRACKING_NUMBER}}|JD0140021234567890|g' \
      -e 's|{{TRACKING_URL}}|https://www.dhl.com/track?id=JD0140021234567890|g' \
      -e 's|{{ESTIMATED_DELIVERY}}|Tuesday 4 August|g' \
      -e 's|{{SHIPPING_ADDRESS}}|12 Bishopsgate, London EC2N 4AJ|g' \
      -e 's|{{SUPPORT_EMAIL}}|help@northwind.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 Northwind Supply order 10428 has shipped' \
      --arg html "$(fill email.html)" \
      --arg text "$(fill email.txt)" \
      '{from: "Northwind Supply <onboarding@emails.sh>",
        to: ["ada@example.com"],
        subject: $subject,
        html: $html,
        text: $text,
        idempotency_key: "shipped-10428-JD0140021234567890",
        tags: {type: "order-shipped"}}')"
```

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

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