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 a markdown field; the server renders it into responsive, email-safe HTML at save time and stores the markdown as source. The html field of the response is the rendered output and cannot be set directly (sending html for this format fails with 422). A stateless POST /v1/templates/render-preview endpoint 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


Edit this page on GitHub

Last updated on