MessagePipedocs
Guides

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:

ChannelRendered fields
Emailsubject, html, text, from_name, from_email, reply_to
SMS / chatbody
Pushtitle, 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:

Template content
{
  "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 subjectResult
{{{subject}}}Your sign-in link — what you want
{{subject}}escaped: Acme&apos;s sign-in link
Fixed subject linethe 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:

Templatedata.subjectDelivered subject
{{subject}}Acme's order <confirmed>Acme&apos;s order &lt;confirmed&gt;
{{{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

HelperSignatureResult
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).