# Trial ending

A trial ending email template that states the date, the price, and what happens to the data. Table-based HTML for Outlook, a text alternative, a preferences link, and code in TypeScript, Python, and curl.

The trial-ending email exists so that nobody is surprised by a charge. That makes it a billing notice with a nudge attached, not a marketing email, and the difference shows in what it leads with: the date, the amount, and the card.

The line that keeps this out of a chargeback dispute is the one saying what happens to the data if they do nothing. Read-only, deleted after ninety days, exported on request: whichever is true, say it here rather than in the terms of service.

Topics: trial ending, subscription, conversion, billing notice.

## When it sends

Three days before the trial converts, on a schedule you run rather than in response to a user action. Send at most twice, and never after they have already subscribed.

## 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 trial end date you trust**: The one your billing system will act on, not the one in your own table.
- **A one-click cancel**: A page where stopping the charge takes one action. Anything harder becomes a dispute.

## Subject

```text
Your {{PRODUCT_NAME}} trial ends in {{DAYS_LEFT}} days
```

## 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 trial is ending</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;">{{PLAN_NAME}} at {{PLAN_PRICE}} starts on {{TRIAL_END_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;">Your trial ends {{TRIAL_END_DATE}}</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 {{PRODUCT_NAME}} trial has {{DAYS_LEFT}} days left. On {{TRIAL_END_DATE}} the account moves to {{PLAN_NAME}} at {{PLAN_PRICE}}, charged to the card ending {{CARD_LAST4}}.</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%">Trial ends</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;">{{TRIAL_END_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%">Plan</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;">{{PLAN_NAME}}</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%">Price</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;">{{PLAN_PRICE}}</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%">Card</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;">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="{{BILLING_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;">Review billing</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="{{BILLING_URL}}" style="color:#3f3f46;text-decoration:underline;word-break:break-all;">{{BILLING_URL}}</a></p>
<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;">If you do nothing and the charge fails, {{DATA_POLICY}}.</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;">Cancelling takes one click on the same page, and there is nothing to explain to anyone.</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;">You are getting this because your {{PRODUCT_NAME}} trial is ending.</p>
<p style="margin:0 0 8px;font-family:Helvetica,Arial,sans-serif;font-size:12px;line-height:18px;color:#71717a;">Choose which emails you get: <a href="{{PREFERENCES_URL}}" style="color:#18181b;text-decoration:underline;">{{PREFERENCES_URL}}</a></p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
```

## Plain text body

```text
Your trial ends {{TRIAL_END_DATE}}

{{FIRST_NAME}}, your {{PRODUCT_NAME}} trial has {{DAYS_LEFT}} days left. On
{{TRIAL_END_DATE}} the account moves to {{PLAN_NAME}} at {{PLAN_PRICE}},
charged to the card ending {{CARD_LAST4}}.

Trial ends:  {{TRIAL_END_DATE}}
Plan:        {{PLAN_NAME}}
Price:       {{PLAN_PRICE}}
Card:        ending {{CARD_LAST4}}

Review billing:
{{BILLING_URL}}

If you do nothing and the charge fails, {{DATA_POLICY}}. Cancelling takes
one click on the same page.

Choose which emails you get: {{PREFERENCES_URL}}
```

## Placeholders

| Placeholder | What it is | Example |
| --- | --- | --- |
| {{PRODUCT_NAME}} | Your product, as the reader knows it. | Acme |
| {{FIRST_NAME}} | What to call them. | Ada |
| {{DAYS_LEFT}} | Days remaining, as a number. | 3 |
| {{TRIAL_END_DATE}} | The date the trial ends, spelled out. | 3 August 2026 |
| {{PLAN_NAME}} | The plan it converts to. | Team |
| {{PLAN_PRICE}} | What it will cost, with the period. | $24 per user per month |
| {{CARD_LAST4}} | The card that will be charged. | 4242 |
| {{DATA_POLICY}} | What happens to the data if they do nothing. | your projects stay read-only for 90 days |
| {{BILLING_URL}} | Where they subscribe, change plan, or cancel. | https://acme.com/settings/billing |
| {{PREFERENCES_URL}} | Where they choose which product emails they get. | https://acme.com/settings/email |

## How it works

1. **A job finds the trials** Everything ending in three days that has not already subscribed.
2. **You check the billing state** A trial that converted this morning must not get this email this afternoon.
3. **You send this** Once, with the date, the price, and the card in the body.
4. **They act or they do not** Either way the next email is a receipt or a cancellation confirmation, not silence.

## 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',
  FIRST_NAME: 'Ada',
  DAYS_LEFT: '3',
  TRIAL_END_DATE: '3 August 2026',
  PLAN_NAME: 'Team',
  PLAN_PRICE: '$24 per user per month',
  CARD_LAST4: '4242',
  DATA_POLICY: 'your projects stay read-only for 90 days',
  BILLING_URL: 'https://acme.com/settings/billing',
  PREFERENCES_URL: 'https://acme.com/settings/email',
};

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}} trial ends in {{DAYS_LEFT}} days'),
  html: fill(readFileSync('email.html', 'utf8')),
  text: fill(readFileSync('email.txt', 'utf8')),
  tags: { type: 'trial-ending', plan: 'team' }
});

// 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",
    "FIRST_NAME": "Ada",
    "DAYS_LEFT": "3",
    "TRIAL_END_DATE": "3 August 2026",
    "PLAN_NAME": "Team",
    "PLAN_PRICE": "$24 per user per month",
    "CARD_LAST4": "4242",
    "DATA_POLICY": "your projects stay read-only for 90 days",
    "BILLING_URL": "https://acme.com/settings/billing",
    "PREFERENCES_URL": "https://acme.com/settings/email",
}


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}} trial ends in {{DAYS_LEFT}} days"),
    html=fill(html),
    text=fill(text),
    tags={"type": "trial-ending", "plan": "team"},
)

# 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|{{FIRST_NAME}}|Ada|g' \
      -e 's|{{DAYS_LEFT}}|3|g' \
      -e 's|{{TRIAL_END_DATE}}|3 August 2026|g' \
      -e 's|{{PLAN_NAME}}|Team|g' \
      -e 's|{{PLAN_PRICE}}|$24 per user per month|g' \
      -e 's|{{CARD_LAST4}}|4242|g' \
      -e 's|{{DATA_POLICY}}|your projects stay read-only for 90 days|g' \
      -e 's|{{BILLING_URL}}|https://acme.com/settings/billing|g' \
      -e 's|{{PREFERENCES_URL}}|https://acme.com/settings/email|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 trial ends in 3 days' \
      --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,
        tags: {type: "trial-ending", plan: "team"}}')"
```

## Questions

### How many trial emails should there be?

Two at most: one three days out and one on the day. A third is a nag, and the person who has not converted after two has decided.

### Is this marketing or transactional?

It is a billing notice, because a charge is about to happen. Treat it as transactional for deliverability and consent, but give it a preferences link anyway, since it is scheduled rather than triggered by the reader.

### Why name the card?

Four digits turn an abstract future charge into one the reader can recognise on a statement, and they answer the question they would otherwise open a ticket to ask.

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