Markdown Templates
Write email templates in markdown and let Tratto render responsive, email-safe HTML for you.
Tratto templates come in two formats: HTML (format: "html", you provide
the HTML) and Markdown (format: "emailmd", you write markdown, Tratto
renders it into responsive, email-safe HTML — dark mode, Outlook fixes and a
plain-text part included).
With a Markdown template:
- The markdown is the source of truth — it lives in the template's
sourcefield. Thehtmlfield is derived from it by the server at save time and cannot be set directly. - Rendering happens when you save, not when you send. The rendered HTML is pinned to the template (and to each version), so delivery is byte-stable.
{{variables}}pass through the render untouched and are substituted at send time, exactly like in HTML templates — including inside link URLs.
Prefer clicking to reading? The dashboard has a full markdown editor with live preview — see Markdown templates in the dashboard.
Syntax
Everything below is standard markdown plus a few email-specific additions. Regular markdown works as you expect:
## A heading
Some **bold** text, a [link](https://example.com), and a list:
- First item
- Second itemFrontmatter
An optional YAML block at the top sets email metadata and theme options:
---
preheader: "Your order has shipped. Track it now"
theme: auto
---preheader— the preview text email clients show next to the subject in the inbox list. It is embedded invisibly in the email body.theme—light(default),dark, orauto(renders light but adapts to readers whose mail client is in dark mode).
Other frontmatter keys (brand_color, button_color, font_family, …) pass
through to the render as well. Invalid values never break the render; they
surface as render warnings instead.
Directives
Directives are fenced blocks that control email layout:
::: header
# Acme Monthly
:::
## Product updates
We shipped **three** improvements this month.
::: callout
Early-bird pricing ends **Friday**.
:::
::: footer
Acme Inc — Milano
[Unsubscribe]({{unsubscribe_url}})
:::::: header— a band above the main content area, typically a logo or brand name. Centered by default; acceptsleft/right.::: callout— a highlighted card for the part people must not miss. Space-separated parameters tune it:center/left/rightfor alignment,compact/spaciousfor padding — e.g.::: callout center compact.::: footer— smaller, muted text at the bottom for legal text, unsubscribe links and company information.
Buttons
Turn any link into a call-to-action button with the {button} attribute:
[Confirm email](https://app.example.com/confirm?token={{token}}){button}
[Learn more](https://example.com){button.secondary}Variables
Use {{variableName}} placeholders anywhere — headings, prose, link URLs.
They survive the markdown render intact and are substituted when the email is
sent, with the variables you pass to POST /v1/emails or a test send.
This is why the saved template's HTML still contains the literal placeholders:
that is by design.
Security: raw HTML is escaped
Markdown templates are rendered with HTML input disabled. Anything that looks
like raw HTML in the markdown (<script>, <img onerror=…>, any tag) is
escaped and shown as literal text, and javascript: / data: links are
never turned into links. If you need hand-written HTML, create a template with
format: "html" instead — the two formats cannot be mixed in one template.
End-to-end: markdown → template → test send → campaign
1. Create the template
Send format: "emailmd" and a markdown field. Do not send html — for
this format it is derived, and providing it fails validation (422).
curl -X POST https://api.tratto.email/v1/templates \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome Email",
"format": "emailmd",
"markdown": "---\npreheader: Your account is ready\n---\n\n# Welcome, {{firstName}}!\n\nThanks for signing up.\n\n[Confirm email](https://app.example.com/confirm?token={{token}}){button}"
}'Response (201):
{
"data": {
"id": "tmpl_abc123",
"name": "Welcome Email",
"status": "draft",
"format": "emailmd",
"source": "---\npreheader: Your account is ready\n---\n\n# Welcome, {{firstName}}!...",
"html": "<!doctype html><html>...</html>",
"renderWarnings": [],
"version": 1,
"createdAt": "2026-08-17T12:00:00Z",
"updatedAt": "2026-08-17T12:00:00Z"
}
}html is the complete rendered document, ready to send. The render also
produces a plain-text part that is delivered as the email's text/plain
alternative. The markdown source is capped at 100,000 characters.
2. Preview without saving
POST /v1/templates/render-preview renders markdown statelessly — it is what
powers the dashboard's live preview:
curl -X POST https://api.tratto.email/v1/templates/render-preview \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{"markdown": "# Hello {{firstName}}"}'Response (200): { "data": { "html": "...", "text": "...", "warnings": [] } }
It requires the same templates:write permission as saving, and has a
tighter rate limit of 60 requests per minute (the render is CPU-bound).
3. Iterate: update re-renders
PATCH with a new markdown re-renders and creates a new version. Sending
html to a Markdown template — or markdown to an html template — fails
with 422. The format itself is fixed at creation and can never be changed.
curl -X PATCH https://api.tratto.email/v1/templates/tmpl_abc123 \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{"markdown": "# Welcome back, {{firstName}}!"}'4. Test send, publish, send
From here on, a Markdown template behaves exactly like an HTML one:
# Test send to your own inbox, with sample variable values
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", "token": "TEST"}}'
# Publish
curl -X PATCH https://api.tratto.email/v1/templates/tmpl_abc123 \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{"status": "published"}'
# Send a transactional email with it
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!",
"templateId": "tmpl_abc123",
"variables": {"firstName": "Alice", "token": "abc123"}
}'Campaigns work the same way: pick the template in the campaign builder and the campaign uses the pinned HTML — click tracking, open pixel and the unsubscribe footer are applied at delivery exactly as for HTML templates.
Sending markdown without a template
POST /v1/emails also accepts a one-off markdown body — no template
involved. See Send Email.
Render warnings
The render never fails on imperfect input — problems degrade gracefully and
are reported as warnings. On a saved template they are stored in
renderWarnings; on render-preview they come back in warnings. Treat them
as a checklist, not as errors: the template still saved and still sends.
Only two situations reject the request outright, both with
VALIDATION_ERROR (422):
- Empty markdown —
"markdown must not be empty." - Conflicting fields —
markdowntogether withhtml, on templates or onPOST /v1/emails.
See Error Codes for the payloads.
Using the SDKs
Markdown support ships in SDK version 1.1.0, which is not published yet — the examples below are a preview of that release. On the current 1.0.0 SDKs, use the REST API directly as shown above.
Node.js (@tratto/email — coming in 1.1.0)
// One-off markdown email — no template
const email = await client.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Welcome!',
markdown: '# Welcome, {{firstName}}!',
variables: { firstName: 'Alice' },
});
// Create a Markdown template
const template = await client.templates.create({
name: 'Welcome Email',
markdown: '# Welcome, {{firstName}}!',
});
console.log(template.format); // 'emailmd'
console.log(template.renderWarnings); // []Python (tratto-email — coming in 1.1.0)
from tratto import SendEmailOptions, CreateTemplateOptions
# One-off markdown email — no template
client.emails.send(SendEmailOptions(
from_='[email protected]',
to='[email protected]',
subject='Welcome!',
markdown='# Welcome, {{firstName}}!',
variables={'firstName': 'Alice'},
))
# Create a Markdown template
template = client.templates.create(CreateTemplateOptions(
name='Welcome Email',
markdown='# Welcome, {{firstName}}!',
))Next Steps
- Prefer the dashboard? See Markdown templates in the dashboard
- Template basics (publish, versions, test send): Templates
- Send one-off markdown: Send Email
- Use the template in a campaign: Campaigns
Edit this page on GitHub
Last updated on