Hosted email templates

A template is a subject, an HTML body, a text body, and a list of the variables they use. You store it once and reference it by slug from POST /v1/emails, from a broadcast, or from an automation step. The content lives in immutable versions, and the version a send uses is the one you published, not the one you last saved.

Saving is not publishing, and that is the whole design

A template row points at a published version. Adding a version never changes what sends. You edit, you preview against sample data, you look at the compatibility warnings, and then you publish, which is a separate call. Versions are immutable, so the copy that went out in March is still readable in March's form. Deleting a template is soft and makes it immediately unsendable rather than removing the history of what it sent.

A missing variable fails the send rather than rendering blank

If a template names {{ first_name }} and the send does not supply it and it has no default, the API returns a 422 naming every variable that is missing. The alternative is an email that goes out saying "Hi ," to forty thousand people, which is a failure nobody notices until a customer screenshots it. Variables are declared with an optional default and an optional required flag, and if you do not declare them they are extracted from the subject and the bodies for you.

Preview fills the gaps so you can look at it

The preview endpoint renders a template, including an unpublished draft, filling any variable you did not supply with a plausible sample value and telling you which ones it filled in. It also returns warnings from an email-HTML compatibility check, which is the list of things that render one way in your browser and another way in Outlook. The render endpoint is the strict sibling: it renders exactly as a send would, and refuses in exactly the same way.

It is not a template language, on purpose

The syntax is {{ variable }} and nothing else. No conditionals, no loops, no expressions, no partials, no filters. A template language in a mail body is a program running against customer data with no test suite and no code review, and the failure mode is a rendering error at send time in front of a customer. If you need logic, do it in your application, where you can test it, and pass the result in as a variable.

The same template serves both halves of the product

POST /v1/emails takes a template object of an id, which may be the slug, and variables. A broadcast takes template_id and optionally template_version_id, so a campaign can be pinned to a specific version. An automation step takes the template by slug in its with block. One place to change the wording, three places it takes effect.

The API

Every one of these exists today and answers to the key you already have. Nothing on this list is planned.

POST /v1/templates
Create a template, optionally with its first version and published in one call.
POST /v1/templates/:id/versions
Add a version. This does not change what sends.
POST /v1/templates/:id/publish
Make a version the one that sends.
POST /v1/templates/:id/preview
Render with samples for anything missing, plus email-HTML warnings.
POST /v1/templates/:id/render
Render exactly as a send would, and fail the same way.

Sending a stored template

The template is referenced by slug. Anything you set alongside it overrides the rendered value.

Shell
curl -sS https://emails.sh/v1/emails \
  -H "Authorization: Bearer $EMAILSSH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@acme.com>",
    "to": ["new.user@example.com"],
    "template": {
      "id": "verification-email",
      "variables": {
        "first_name": "Ada",
        "verify_url": "https://acme.com/verify?token=8f2c1a"
      }
    },
    "idempotency_key": "verify:8f2c1a"
  }'

What this does not do

Named here rather than left for a trial to discover. If something on this list is how your team works, that is a real reason to pick another product, and we would rather you knew now.

There is no visual builder

You write the HTML, or your assistant does. There is no drag-and-drop editor, no block library, and no MJML compiler. If somebody who does not write code needs to change the wording without a deploy, they can edit a template in the dashboard, but they are editing HTML.

No conditionals, loops, or partials

{{ variable }} is the entire syntax. A receipt with a variable number of line items has to arrive as a pre-rendered block of HTML in a variable, or be sent as html on the request instead.

There is no template test-send endpoint

Preview and render return the rendered output, and the only endpoint that puts a test copy in your own inbox is the broadcast test. To try a transactional template for real, send it to yourself with POST /v1/emails.

No localisation and no per-locale variants

One template is one language. Multiple languages means multiple templates and choosing the slug in your own code.

Questions

Can I keep templates in my repository instead?

Yes, and for a lot of teams that is the right answer: pass html and text on the request and the markup lives next to the code that sends it, in review, in your test suite. Hosted templates exist for the cases where the copy has to change without a deploy, and for broadcasts and automations, which need something to point at.

What happens to mail already sent when I publish a new version?

Nothing. A message is rendered at send time and the result is what was delivered. Versions are immutable, so the exact content of an older send is still readable through the version that produced it.

Can I use React Email or MJML?

Compile it yourself and store or send the output. This product takes HTML and text. Anything that produces HTML works, and nothing about the toolchain is our business.

Does a template know about the unsubscribe link?

Broadcasts add the List-Unsubscribe headers themselves, on every copy, whatever the template says. If you want a visible unsubscribe link in the body, put one in the template.

Send your first email today.

3,000 emails a month on the transactional side and 1,000 contacts on the marketing side, both free, no card. Enough to run real traffic through it before you decide.