# Welcome

A welcome email template that names one next step and one place to get help. Table-based HTML for Outlook, a plain-text alternative, a preferences link, and the send call in TypeScript, Python, and curl.

The welcome email is read once, in the thirty seconds after signup, by somebody who has already decided to try the thing. It does not need to sell. It needs to point at the one action that makes the product work for them.

This one names that action, links the docs, and gives a real address to reply to. Everything else that products put in a welcome email, the feature grid, the founder photo, the discount, either belongs later in a sequence or nowhere.

Topics: welcome email, onboarding, activation, first run.

## When it sends

Once, after the address is verified and the account exists. Sending it before verification means welcoming an address that may never confirm.

## 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.
- **One next step worth naming**: The action that correlates with people staying. Pick it from your own data, not from a list.
- **A reply-to somebody reads**: A welcome email gets replies. Point them at a mailbox with a person behind it.

## Subject

```text
Welcome to {{PRODUCT_NAME}}
```

## 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>Welcome</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;">Start here: {{NEXT_STEP}}.&#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;">Welcome to {{PRODUCT_NAME}}, {{FIRST_NAME}}</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;">Your account is ready. The one thing worth doing first is to {{NEXT_STEP}}.</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 20px;"><tr><td bgcolor="#18181b" style="background-color:#18181b;"><a href="{{DASHBOARD_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;">Open {{PRODUCT_NAME}}</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="{{DASHBOARD_URL}}" style="color:#3f3f46;text-decoration:underline;word-break:break-all;">{{DASHBOARD_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;">The quickstart walks the same path in about five minutes: <a href="{{DOCS_URL}}" style="color:#18181b;text-decoration:underline;">{{DOCS_URL}}</a>.</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 20px;"><tr><td height="1" style="height:1px;background-color:#e4e4e7;font-size:0;line-height:0;">&nbsp;</td></tr></table>
<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 get stuck, reply to this email. It goes to <a href="mailto:{{SUPPORT_EMAIL}}" style="color:#18181b;text-decoration:underline;">{{SUPPORT_EMAIL}}</a> and a person reads it.</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 you created a {{PRODUCT_NAME}} account.</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
Welcome to {{PRODUCT_NAME}}, {{FIRST_NAME}}

Your account is ready. The one thing worth doing first is to {{NEXT_STEP}}.

{{DASHBOARD_URL}}

The quickstart walks the same path in about five minutes:
{{DOCS_URL}}

If you get stuck, reply to this email. It goes to {{SUPPORT_EMAIL}} and a
person reads it.

You are getting this because you created a {{PRODUCT_NAME}} account.
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. Fall back to "there". | Ada |
| {{NEXT_STEP}} | The one action worth taking first, in a few words. | connect your first repository |
| {{DASHBOARD_URL}} | Where that action starts. | https://acme.com/app |
| {{DOCS_URL}} | The quickstart, not the docs home. | https://acme.com/docs/quickstart |
| {{SUPPORT_EMAIL}} | The reply-to. A real mailbox with a person behind it. | support@acme.com |
| {{PREFERENCES_URL}} | Where they choose which product emails they get. | https://acme.com/settings/email |

## How it works

1. **The address is verified** Only then is there somebody to welcome.
2. **You pick the next step** One action, chosen from what your retained accounts did first.
3. **You send this** With reply_to pointed at a mailbox somebody reads.
4. **They reply, sometimes** Answer those. The replies to a welcome email are the cheapest research you will get.

## 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',
  NEXT_STEP: 'connect your first repository',
  DASHBOARD_URL: 'https://acme.com/app',
  DOCS_URL: 'https://acme.com/docs/quickstart',
  SUPPORT_EMAIL: 'support@acme.com',
  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('Welcome to {{PRODUCT_NAME}}'),
  html: fill(readFileSync('email.html', 'utf8')),
  text: fill(readFileSync('email.txt', 'utf8')),
  replyTo: 'support@acme.com',
  tags: { type: 'welcome' }
});

// 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",
    "NEXT_STEP": "connect your first repository",
    "DASHBOARD_URL": "https://acme.com/app",
    "DOCS_URL": "https://acme.com/docs/quickstart",
    "SUPPORT_EMAIL": "support@acme.com",
    "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("Welcome to {{PRODUCT_NAME}}"),
    html=fill(html),
    text=fill(text),
    reply_to="support@acme.com",
    tags={"type": "welcome"},
)

# 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|{{NEXT_STEP}}|connect your first repository|g' \
      -e 's|{{DASHBOARD_URL}}|https://acme.com/app|g' \
      -e 's|{{DOCS_URL}}|https://acme.com/docs/quickstart|g' \
      -e 's|{{SUPPORT_EMAIL}}|support@acme.com|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 'Welcome to Acme' \
      --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,
        reply_to: "support@acme.com",
        tags: {type: "welcome"}}')"
```

## Questions

### Should the welcome email come from a person?

A person as the display name and a monitored mailbox as the address works well. What does not work is a name with noreply@ behind it, which invites a reply into a black hole.

### Why only one link to the product?

Because a welcome email with six equal links gets none of them clicked. The docs link is there in body text at a lower weight, which is a deliberate second place rather than a second button.

### Does a welcome email need an unsubscribe?

It needs a way out of the sequence it starts. This one links preferences instead of a bare unsubscribe, so somebody can stop onboarding mail without also opting out of receipts and password resets.

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