Trial ending

What happens on the date, what it costs, and how to stop it.

Get an API key

Billing · 10 placeholders

The job

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.

How this one works

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.

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

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.

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

A trial end date you trust

The one your billing system will act on, not the one in your own table.

04

A one-click cancel

A page where stopping the charge takes one action. Anything harder becomes a dispute.

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
Your {{PRODUCT_NAME}} trial ends in {{DAYS_LEFT}} days
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>

How it works

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

01

A job finds the trials

Everything ending in three days that has not already subscribed.

02

You check the billing state

A trial that converted this morning must not get this email this afternoon.

03

You send this

Once, with the date, the price, and the card in the body.

04

They act or they do not

Either way the next email is a receipt or a cancellation confirmation, not silence.

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

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.

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.

More billing templates