Templates: Create, Version & Test
Manage versioned email templates with variable substitution
Templates are reusable email templates with placeholder variables. Instead of composing HTML in code every time, define a template once and send it hundreds of times with different variables.
Two Formats: HTML and Markdown
Every template has a format, chosen at creation and immutable afterwards:
html(default) — you provide the email HTML yourself. Everything on this page uses this format.emailmd— you provide markdown in amarkdownfield; the server renders it into responsive, email-safe HTML at save time and stores the markdown assource. Thehtmlfield of the response is the rendered output and cannot be set directly (sendinghtmlfor this format fails with 422). A statelessPOST /v1/templates/render-previewendpoint renders markdown without saving.
Markdown templates get their own guide — syntax, security model, end-to-end
flow: Markdown Templates. Responses on
this page omit the format field for brevity; templates created before
formats existed are html.
Create a Template
A template is a name plus a body. The request accepts name, format,
and one of html or markdown — nothing else.
A template has no subject. There is no subject field on a template, and
no text field you can set: the subject belongs to each send. Sending one in
this request has no effect — it is silently ignored, not stored.
cURL
curl -X POST https://api.tratto.email/v1/templates \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{
"name":"Welcome Email",
"html":"<h1>Welcome, {{firstName}}!</h1><p>Your code: {{code}}</p>"
}'Response:
{
"data": {
"id": "tmpl_abc123",
"name": "Welcome Email",
"html": "<h1>Welcome, {{firstName}}!</h1><p>Your code: {{code}}</p>",
"status": "draft",
"version": 1,
"createdAt": "2025-06-30T12:00:00Z",
"updatedAt": "2025-06-30T12:00:00Z"
}
}status is a label, not a gate
A template is created with status: "draft", and status accepts draft or
published. That is the whole of it: it is an editorial label for your own
workflow, and nothing in the API enforces it.
A draft template sends. The delivery path loads the template and reads
its html; it never looks at status. If you need "not ready to send" to
actually mean that, enforce it in your own code — do not rely on the API to
refuse.
Change the label like any other field:
cURL
curl -X PATCH https://api.tratto.email/v1/templates/tmpl_abc123 \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{"status":"published"}'It is also a filter on the list endpoint: GET /v1/templates?status=published.
Send Using a Template
The template supplies the body. subject is required on the send and has
to be passed every time — omit it and the request fails with 422.
cURL
curl -X POST https://api.tratto.email/v1/emails \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{
"from":"[email protected]",
"to":"[email protected]",
"subject":"Welcome, {{firstName}}!",
"templateId":"tmpl_abc123",
"variables":{
"firstName":"Alice",
"code":"SECRET123"
}
}'Renders to:
Subject: Welcome, Alice!
HTML: <h1>Welcome, Alice!</h1><p>Your code: SECRET123</p>The subject is rendered with the same variables as the body, so
{{firstName}} works in both. A variable you don't pass becomes an empty
string in the subject too — see Always Pass Every Variable below.
An html template has no text part. A Markdown template pins a rendered
text part at save time, and that one is sent as text/plain alongside the
HTML.
Update a Template
Update a template. This creates a new version, leaving previous versions intact.
cURL
curl -X PATCH https://api.tratto.email/v1/templates/tmpl_abc123 \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{
"html":"<h1>Welcome back, {{firstName}}!</h1><p>Code: {{code}}</p>"
}'Response: the full template, with version incremented.
{
"data": {
"id": "tmpl_abc123",
"name": "Welcome Email",
"html": "<h1>Welcome back, {{firstName}}!</h1><p>Code: {{code}}</p>",
"status": "draft",
"version": 2,
"createdAt": "2025-06-30T12:00:00Z",
"updatedAt": "2025-06-30T12:05:00Z"
}
}The version only bumps when the content actually changes — a PATCH that sets
the same HTML leaves it alone. Versions have no status of their own, and
bumping the version does not touch status on the template.
For Markdown templates, update the markdown field instead — every change
re-renders the HTML and increments the version the same way. html and
markdown are mutually exclusive in a PATCH, and each one is only accepted by
a template of the matching format (422 otherwise).
Version History
List the versions of a template, newest first (up to 20). Each entry carries only the version number and when it was saved:
cURL
curl https://api.tratto.email/v1/templates/tmpl_abc123/versions \
-H "Authorization: Bearer tratto_live_..."Response:
{
"data": [
{ "version": 2, "savedAt": "2025-06-30T12:05:00Z" },
{ "version": 1, "savedAt": "2025-06-30T12:00:00Z" }
]
}Get One Version
To read the content of a past version — the only way to recover it — fetch it by number:
cURL
curl https://api.tratto.email/v1/templates/tmpl_abc123/versions/1 \
-H "Authorization: Bearer tratto_live_..."Response:
{
"data": {
"version": 1,
"html": "<h1>Welcome, {{firstName}}!</h1><p>Your code: {{code}}</p>",
"savedAt": "2025-06-30T12:00:00Z"
}
}On a Markdown template the response also carries source: the markdown that
produced that version's HTML.
Test Send
Send a test email using the template to verify it looks correct.
cURL
curl -X POST https://api.tratto.email/v1/templates/tmpl_abc123/test-send \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{
"to":"[email protected]",
"variables":{
"firstName":"Alice",
"code":"TEST123"
}
}'A test email is sent immediately to your address so you can review it in your inbox.
Its subject is [Test] followed by the template name. A test send has no
contact, so the recipient variables you don't pass get preview values: email
is the test address, firstName is Preview, lastName is Recipient, and
unsubscribe_url is a preview link that unsubscribes nobody. Any other
variable you leave out renders empty, as on a real send.
List Templates
Get all templates for your tenant.
cURL
curl https://api.tratto.email/v1/templates \
-H "Authorization: Bearer tratto_live_..."Get a Template
Retrieve a specific template.
cURL
curl https://api.tratto.email/v1/templates/tmpl_abc123 \
-H "Authorization: Bearer tratto_live_..."Delete a Template
Permanently delete a template (cannot be undone).
cURL
curl -X DELETE https://api.tratto.email/v1/templates/tmpl_abc123 \
-H "Authorization: Bearer tratto_live_..."Deleting a template cannot be undone. Campaigns and flows using that template will fail.
Template Variable Syntax
Use double curly braces {{variableName}} for placeholders — surrounding
whitespace is allowed, so {{ firstName }} works too. Variables are
case-sensitive. There are no conditionals and no loops: this is substitution,
not a templating language.
Template:
HTML: <p>Hello {{firstName}}, your email is {{email}}</p>Sending:
{
"variables": {
"firstName": "Alice",
"email": "[email protected]"
}
}Rendered:
HTML: <p>Hello Alice, your email is [email protected]</p>Substitution applies to the subject and to the HTML and text bodies.
Best Practices
1. Use Semantic Variable Names
✅ {{firstName}}, {{orderID}}, {{confirmationCode}}
❌ {{x}}, {{var1}}, {{v}}
2. Always Pass Every Variable
A missing variable does not fail the send: the placeholder is replaced with an empty string. This template
<p>Hello {{firstName}}, welcome to {{companyName}}!</p>sent without firstName or companyName arrives as Hello , welcome to ! —
the punctuation is left stranded, but the recipient never sees the name of
your variable.
This applies to the subject and the body, in HTML and in Markdown, on
transactional sends and campaigns alike. null substitutes to empty too: the
strings null and undefined never reach an inbox.
None of which is an invitation to pass fewer variables — quite the opposite.
A sentence with a hole in it is still a wrong sentence, and now nothing
flags it: not at send time, not before. With the placeholder left visible, a
typo like {{firstname}} for {{firstName}} at least showed up in the
preview; with empty substitution it vanishes without a trace.
The automatic check exists for campaigns only, where Tratto knows the set
of available variables (email, firstName, lastName, unsubscribe_url)
and can flag anything outside it while the campaign is still a draft. For a
template it isn't possible: the variables come from whoever calls
POST /v1/emails, so there is no set to check against and every check would
be a guess.
On a template, then, the only defence is passing every placeholder the template uses on every send.
3. Use Templates for Bulk Sends
Don't compose HTML per email. Use templates and variables for consistency and reusability.
4. Test Before You Use It
Always test-send a template before you send it to anyone, to catch rendering
issues. Nothing stops a draft template from going out, so a test-send is the
only check there is.
Next Steps
- Send with a template? See Send Email
- Write templates in markdown? See Markdown Templates
- Use templates in campaigns? Go to Campaigns
- Use templates in flows? See Flows
Edit this page on GitHub
Last updated on