Template syntax
Write dynamic content with Handlebars variables, conditionals, loops, and helpers.
MessagePipe templates are rendered with Handlebars. You author a template
in the dashboard, then reference it on a send with the template field and
pass values through the data object:
curl https://api.dev.messagepipe.dev/v1/email/send \
-H "Authorization: Bearer mp_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"to": "maya@example.com",
"template": "order.confirmation",
"data": { "first_name": "Maya", "order": { "id": "1234", "total": 4200 } }
}'Everything under data is available to the template.
What a template renders
Every field of the template is a Handlebars source string, rendered against
data — not just the body:
| Channel | Rendered fields |
|---|---|
subject, html, text, from_name, from_email, reply_to | |
| SMS / chat | body |
| Push | title, body |
On a template send, data is the only channel into the message body. A
top-level subject or html on the request is ignored — the template's
fields win. The envelope is the exception: from, reply_to, headers and
attachments ride on the request, not the template. A request from or
reply_to overrides the template's from_name / from_email / reply_to,
and the template doesn't have to reference them (see
Email).
Setting the subject on a template send
There is no request-level subject override. subject at the top level of a
template send is silently ignored, so a dynamic subject line has to be a
variable in the template's own subject field:
{
"subject": "{{{subject}}}",
"html": "{{{html}}}",
"text": "{{{text}}}"
}Then pass it in data like any other variable:
curl https://api.dev.messagepipe.dev/v1/email/send \
-H "Authorization: Bearer mp_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"to": "maya@example.com",
"template": "sign-in-link",
"data": {
"subject": "Your sign-in link",
"html": "<h1>Sign in</h1><p>This renders as <strong>markup</strong>.</p>",
"text": "Sign in — plain-text fallback."
}
}'Two ways this goes wrong, both silent:
Template's subject | Result |
|---|---|
{{{subject}}} | Your sign-in link — what you want |
{{subject}} | escaped: Acme's sign-in link |
Fixed subject line | the literal string; data.subject is inert |
| (field absent) | empty subject line, no error |
A template whose subject is hardcoded cannot be overridden per send. If you
need the subject to vary, the template has to say so.
A top-level "subject" alongside "template" is not an error — it is parsed,
stored on the send, and never read. It looks like it works right up until you
check the inbox.
Variables
Reference a value with double braces. Dot notation walks nested objects:
Hi {{first_name}}, your order {{order.id}} is confirmed.Double braces HTML-escape the value, which is what you want inside an
html body. To emit raw, pre-trusted HTML, use triple braces:
{{{ banner_html }}}Escaping applies to every field, including ones that are not HTML. A subject
line written {{subject}} arrives in the inbox with its punctuation escaped:
| Template | data.subject | Delivered subject |
|---|---|---|
{{subject}} | Acme's order <confirmed> | Acme's order <confirmed> |
{{{subject}}} | Acme's order <confirmed> | Acme's order <confirmed> |
Use triple braces for subject, from_name, and reply_to. Use double braces
inside html unless you are deliberately interpolating markup.
Double braces escape exactly five characters — &, ', <, >, " — and
no others. Unlike handlebars.js, a backtick and an = pass through unchanged.
That is safe between tags and in a quoted attribute, but an interpolation into
an unquoted attribute (<a href={{url}}>) is not protected. Quote your
attributes.
A variable missing from data renders as an empty string, silently — no
error, no missing_variables. Marking a variable required in the template's
variables_schema documents intent but is not enforced at send time, so a
forgotten data.subject ships an email with a blank subject line. Use the
default helper to make the fallback explicit.
Passthrough templates
A passthrough template stores no copy of its own: it interpolates the subject and body the caller supplies. Its content is just variables —
{
"subject": "{{{subject}}}",
"html": "{{{html}}}",
"from_email": "orders@your-domain.com"
}— and every send fills them in:
curl https://api.dev.messagepipe.dev/v1/email/send \
-H "Authorization: Bearer mp_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"to": "maya@example.com",
"template": "generic-html",
"data": {
"subject": "Order #1234 confirmed",
"html": "<p>Pretty body, served when the client accepts HTML.</p>"
}
}'This buys a template's benefits — a pinned sender, versioning, an audit trail of which version rendered each send — without committing to fixed copy.
Both fields must be triple-braced. A double-braced {{html}} escapes your
markup and delivers it as visible text, which is the most common way a
passthrough template goes wrong.
Triple braces mean you own the escaping. A passthrough template
interpolates caller-supplied markup verbatim into an email your domain signs
with DKIM. Never pass unsanitised user input through data.html.
Conditionals
{{#if order.expedited}}
Your order ships today.
{{else}}
Your order ships within two business days.
{{/if}}
{{#unless user.verified}}Please confirm your email.{{/unless}}Combine with the comparison helpers for richer conditions:
{{#if (gt cart.total 100)}}You qualify for free shipping.{{/if}}
{{#if (eq plan "pro")}}Thanks for being on Pro.{{/if}}Loops
{{#each items}}
- {{this.name}} x{{this.qty}}
{{/each}}Inside {{#each}}, {{this}} is the current element and {{this.field}}
reaches its properties.
Helpers
| Helper | Signature | Result |
|---|---|---|
eq | (eq a b) | true when a equals b |
ne | (ne a b) | true when a does not equal b |
gt | (gt a b) | true when a is greater than b (numeric) |
lt | (lt a b) | true when a is less than b (numeric) |
default | (default value fallback) | value, or fallback when it is missing or empty |
truncate | (truncate value n) | first n characters, then an ellipsis |
urlEncode | (urlEncode value) | URL-encoded string (spaces as %20) |
formatNumber | (formatNumber value) | thousands-grouped integer, e.g. 1,250 |
formatCurrency | (formatCurrency value currency) | symbol plus two decimals, e.g. $42.00 (USD, EUR, GBP) |
formatDate | (formatDate value) | M/D/YYYY from an RFC 3339 string or epoch milliseconds |
Examples:
Hi {{default first_name "there"}},
Your total is {{formatCurrency order.total "USD"}} on {{formatDate order.placed_at}}.
Tracking: https://acme.dev/t?id={{urlEncode order.id}}Versions
Templates are versioned. A send uses the current published version unless you
pin one with template_version. Editing a template creates a new version, so
in-flight sends are never affected by a change you make mid-flight
(Templates).