Reference
Templates
Store the content here and send it by name. Changing the wording of a receipt stops being a deploy of your application, and a delivery record can say what the message actually contained — even after the template has moved on.
A published version never changes
Editing a template publishes a new version. The old one stays exactly as it was, because messages point at it. That is the whole reason templates are versioned rather than editable: a delivery record that cannot say what it contained is not a record.
A send pins the version it used. Edit the template ten times afterwards and last month’s message still reports the content that actually went out.
The database refuses an update to a published version, not just the API. There is no path — including a direct UPDATE — that rewrites what a message said.
Variables are declared, and a missing one is refused
Write {{first_name}} in the subject, the HTML or the text, and declare it in variables. Declared rather than inferred, because inferring the list from the body means a typo silently becomes a new optional variable.
A missing required variable is a 422, never a blank. Most template engines resolve an unknown variable to an empty string, so a typo sends “Hi ,” to a customer and nothing anywhere reports a problem. You would find out from a support ticket. We refuse the send instead and name every variable that was missing.
Mark one { "name": "x", "required": false } to have it render as empty when absent. A tag used in the body but never declared is refused at publish time, so {{frist_name}} is caught by whoever is editing rather than by whoever receives the blank.
Escaping follows the part
| Part | Escaped? | Why |
|---|---|---|
html | Yes, including both quote characters | A value lands inside markup you wrote, and one of the places it lands is an attribute. |
subject | No | It is a header, not markup. & in a subject line is a visible bug in every mail client. |
text | No | The reader is looking at raw text by definition. |
Substitution is all this is. There are no loops, conditionals or expressions — a body is authored by whoever holds an API key, and the moment it can branch it can also probe.
Endpoints
/v1/templatestemplates:write/v1/templatestemplates:read/v1/templates/:idtemplates:read/v1/templates/:id/versionstemplates:read/v1/templates/:id/previewtemplates:read/v1/templates/:idtemplates:write/v1/templates/:idtemplates:writePATCH with any content field publishes a new version. A name on its own is a rename and creates nothing — what a template is called is not part of what a message said.
DELETE is refused once anything has been sent from the template. Deleting it would erase what those messages contained.
How many templates you may store is set by your plan: Free 3, Starter 25, Growth 100, Scale unlimited, Enterprise unlimited. Creating one past the cap returns 403 with "type": "template_limit_reached", carrying the limit and how many you have. Versions are not counted — revising a template you are already allowed is not a new template. The full grid is in limits.
Preview renders; it does not send
POST /v1/templates/:id/preview returns the rendered subject, HTML and text. It reports a render failure exactly as a send does — same error type, same list of missing names — so a preview that passes means the send will too.
It deliberately sends nothing. To test for real, send it to yourself: that path has the suppression check, the warmup throttle and the DKIM signing on it, and a preview that skipped those would be testing something other than production.
The response also carries lint — the same pre-send checks POST /v1/emails runs, on the same rendered bytes. lint.blocked means a send of this content would be refused; everything else in lint.findings is advice you are free to ignore. Two checks stay quiet in a preview because it cannot know the answer: it has no listUnsubscribe and no stream, so it never asks for a one-click unsubscribe, and it has no attachments, so an empty-bodied template reads as empty even though a send carrying a file is accepted.
Examples
POST /v1/templates
{
"slug": "password-reset",
"name": "Password reset",
"subject": "Reset your password, {{first_name}}",
"html": "<p>Hi {{first_name}}, <a href=\"{{link}}\">reset it here</a>.</p>",
"text": "Hi {{first_name}}, reset it here: {{link}}",
"variables": [
{ "name": "first_name" },
{ "name": "link" }
]
}
201 {
"id": "tpl_AZLm3kQ8T2Sf9pXbNc7HrQ",
"slug": "password-reset",
"latestVersion": { "version": 1, "publishedAt": "2026-08-25T12:00:00.000Z", … }
}NextMessage streams →Separate transactional mail from everything else, so a newsletter complaint never withholds a password reset.