Skip to content

Templates

You write your own HTML. A template holds numbered versions, and a campaign sends one exact version, so what you tested is what goes out. Versions never change once they’re created.

Templates use the Liquid template language, with these values:

Value What it is
{{ contact.fields.FNAME }} A custom field. Use a default for contacts without it: {{ contact.fields.FNAME | default: "there" }}
{{ contact.email }} The contact’s address
{{ contact.tags }} The contact’s tags, for example {% if contact.tags contains "vip" %}
{{ unsubscribe_url }} The contact’s own unsubscribe link. Required in the HTML.
{{ footer_address }} Your physical mailing address from settings. Required in the HTML.

Values in the HTML are escaped; the subject isn’t. Templates can’t include files or other templates: include, render, layout and block aren’t allowed. Each version needs a subject, html, fromName and a fromEmail on your sending domain, and can have a preheader, a plain-text text part and a replyTo. Without text, we generate it from the HTML.

Every email also gets List-Unsubscribe and List-Unsubscribe-Post headers for one-click unsubscribe, a Feedback-ID and Precedence: bulk. You don’t add them.

Terminal window
curl -X POST "$BASE/templates" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{
"name": "Monthly update",
"version": {
"subject": "Your {{ contact.fields.plan | default: \"account\" }} update for October",
"preheader": "What changed this month, in two minutes.",
"html": "<p>Hi {{ contact.fields.FNAME | default: \"there\" }},</p><p>Here is what is new.</p><p><a href=\"{{ unsubscribe_url }}\">Unsubscribe</a><br>{{ footer_address }}</p>",
"fromName": "Example Co", "fromEmail": "news@example.com", "replyTo": "hello@example.com"
}
}'

The response is the template with version 1. Check its warnings: you get one when the HTML is over 102 KB, because Gmail clips larger messages, and one when there’s no preheader.

Check it worked: the template has an id starting tp_ and latestVersion 1.

A template without the unsubscribe link or the footer is refused:

Terminal window
# Expect 422 template_invalid
curl -X POST "$BASE/templates" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"name":"No footer","version":{"subject":"Hi","html":"<p>Hi</p>","fromName":"Example Co","fromEmail":"news@example.com"}}'

Render the latest version with sample values:

Terminal window
curl -X POST "$BASE/templates/<tp_...>/render" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"sample":{"fields":{"FNAME":"Ada","plan":"pro"}}}'

Or with one of your contacts. Get a contact ID first:

Terminal window
curl "$BASE/contacts?limit=1" -H "Authorization: Bearer $KEY"
curl -X POST "$BASE/templates/<tp_...>/render" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"contactId":"<ct_...>"}'

The response has the subject, html, text and headers exactly as the email would be sent. A Liquid error gives 422 render_failed.

Terminal window
curl -X POST "$BASE/templates/<tp_...>/versions" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{
"subject": "Your October update",
"preheader": "What changed this month, in two minutes.",
"html": "<p>Hi {{ contact.fields.FNAME | default: \"there\" }},</p><p>Here is what is new, now with pictures.</p><p><a href=\"{{ unsubscribe_url }}\">Unsubscribe</a><br>{{ footer_address }}</p>",
"fromName": "Example Co", "fromEmail": "news@example.com"
}'
curl "$BASE/templates/<tp_...>" -H "Authorization: Bearer $KEY"

Check it worked: the template shows latestVersion 2, and versions lists the newest 20, newest first. GET /templates/<tp_...>/versions lists every version, newest first, a page at a time.

fromEmail must be on your verified sending domain:

Terminal window
# Expect 422 from_domain_unverified
curl -X POST "$BASE/templates" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"name":"Wrong domain","version":{"subject":"Hi","html":"<a href=\"{{ unsubscribe_url }}\">u</a> {{ footer_address }}","fromName":"Example Co","fromEmail":"news@example.org"}}'

Send yourself a test with POST /campaigns/{id}/test before you schedule.