# Account deleted

An account deletion confirmation template stating what was deleted, what is retained and for how long, and how to recover in time. Table-based HTML, a text alternative, and code in TypeScript, Python, and curl.

This is the last email the account will get, and it does two jobs at once. It confirms to the person who asked that the deletion happened, and it warns the person who did not ask that somebody deleted their account.

The retention window is the content. A deletion email that says "your data has been removed" without a date leaves the reader unable to tell whether an export is still possible, which is the single question they will write back with.

Topics: account deletion, gdpr, data retention, offboarding.

## When it sends

Once, when the deletion has actually run rather than when it was requested. Send it to the address on the account even though that address no longer signs in anywhere.

## 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 retention window you honour**: The real number of days before the data is unrecoverable, from your own deletion job.
- **A recovery path while it lasts**: One address a person can write to inside the window, staffed by somebody who can undo it.

## Subject

```text
Your {{PRODUCT_NAME}} account has been deleted
```

## 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>Account deleted</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;">Recoverable until {{PURGE_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 account has been deleted</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;">The {{PRODUCT_NAME}} account for {{ACCOUNT_EMAIL}} was deleted on {{DELETED_DATE}}. Signing in is no longer possible and the data is out of the product.</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%">Account</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;">{{ACCOUNT_EMAIL}}</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%">Deleted</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;">{{DELETED_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%">Recoverable until</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;">{{PURGE_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%">Billing</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;">{{BILLING_NOTE}}</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;">Backups are kept for {{RETENTION_DAYS}} days. Until {{PURGE_DATE}} we can still restore the account if this was a mistake. After that date nothing can be recovered by anyone.</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="mailto:{{SUPPORT_EMAIL}}" 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;">Write to support</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;">If you did not ask for this, write to <a href="mailto:{{SUPPORT_EMAIL}}" style="color:#18181b;text-decoration:underline;">{{SUPPORT_EMAIL}}</a> today. Somebody with access to the account requested the deletion, and the window to undo it is short.</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;">{{PRODUCT_NAME}} sent this to the address on the deleted account.</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
```

## Plain text body

```text
Your account has been deleted

The {{PRODUCT_NAME}} account for {{ACCOUNT_EMAIL}} was deleted on
{{DELETED_DATE}}. Signing in is no longer possible.

Account:            {{ACCOUNT_EMAIL}}
Deleted:            {{DELETED_DATE}}
Recoverable until:  {{PURGE_DATE}}
Billing:            {{BILLING_NOTE}}

Backups are kept for {{RETENTION_DAYS}} days. Until {{PURGE_DATE}} we can
still restore the account if this was a mistake. After that date nothing
can be recovered by anyone.

If you did not ask for this, write to {{SUPPORT_EMAIL}} today.
```

## Placeholders

| Placeholder | What it is | Example |
| --- | --- | --- |
| {{PRODUCT_NAME}} | Your product, as the reader knows it. | Acme |
| {{ACCOUNT_EMAIL}} | The account that was deleted. | ada@example.com |
| {{DELETED_DATE}} | When the deletion ran. | 31 July 2026 |
| {{RETENTION_DAYS}} | Days before backups are unrecoverable. | 30 |
| {{PURGE_DATE}} | The date after which nothing can be restored. | 30 August 2026 |
| {{BILLING_NOTE}} | What happened to any subscription. | Your Team subscription was cancelled and will not renew |
| {{SUPPORT_EMAIL}} | Who can undo this while the window is open. | support@acme.com |

## How it works

1. **Somebody asks for deletion** You confirm it in the product, then queue the job.
2. **The job runs** Live data goes first. Backups age out on their own schedule.
3. **You send this** After the deletion, with the purge date the backup schedule actually produces.
4. **The window closes** On the purge date the account is unrecoverable, exactly as this email said.

## 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',
  ACCOUNT_EMAIL: 'ada@example.com',
  DELETED_DATE: '31 July 2026',
  RETENTION_DAYS: '30',
  PURGE_DATE: '30 August 2026',
  BILLING_NOTE: 'Your Team subscription was cancelled and will not renew',
  SUPPORT_EMAIL: 'support@acme.com',
};

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}} account has been deleted'),
  html: fill(readFileSync('email.html', 'utf8')),
  text: fill(readFileSync('email.txt', 'utf8')),
  replyTo: vars.SUPPORT_EMAIL,
  tags: { type: 'account-deleted' }
});

// 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",
    "ACCOUNT_EMAIL": "ada@example.com",
    "DELETED_DATE": "31 July 2026",
    "RETENTION_DAYS": "30",
    "PURGE_DATE": "30 August 2026",
    "BILLING_NOTE": "Your Team subscription was cancelled and will not renew",
    "SUPPORT_EMAIL": "support@acme.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_="Acme <onboarding@emails.sh>",
    to=["ada@example.com"],
    subject=fill("Your {{PRODUCT_NAME}} account has been deleted"),
    html=fill(html),
    text=fill(text),
    reply_to=values["SUPPORT_EMAIL"],
    tags={"type": "account-deleted"},
)

# 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|{{ACCOUNT_EMAIL}}|ada@example.com|g' \
      -e 's|{{DELETED_DATE}}|31 July 2026|g' \
      -e 's|{{RETENTION_DAYS}}|30|g' \
      -e 's|{{PURGE_DATE}}|30 August 2026|g' \
      -e 's|{{BILLING_NOTE}}|Your Team subscription was cancelled and will not renew|g' \
      -e 's|{{SUPPORT_EMAIL}}|support@acme.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 Acme account has been deleted' \
      --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: "account-deleted"}}')"
```

## Questions

### Why send anything to a deleted account?

Because deletion is the most damaging action an attacker can take with a stolen session, and this email is the only signal the real owner gets. It is also the record that proves you acted on a request.

### Is it safe to keep the address after deletion?

Keep the minimum needed to send this and to honour a suppression list, and say so in your privacy notice. Deleting the address entirely means you can no longer prove the deletion happened or stop mailing them again.

### What if the deletion is immediate and total?

Then say that, and drop the recovery paragraph. What matters is that the email describes the retention your systems actually have, not a comfortable number.

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