Template Language

The SparkPost API provides a powerful handlebars-style template language that you can use in the email subject, headers, text, HTML, and AMP HTML content.

Features include:

Template Variables

You can pass in JSON data to a template through both the transmission and individual recipient. The data can be anything from a simple string or deeply nested arrays and objects. That data can then be used in expressions or statements to generate the email.

Keys
The substitution keys can be contain any US-ASCII letters, digits, and underscores, not beginning with a digit, with the exception of the following keywords:

address, email, email_id, env_from, return_path, and, break, do, else, elseif, end, false, for, function, if, local, nil ,not, or, each, return, then, true, until, while

Values
The substitution values can be any valid UTF-8 string or JSON value.

Sources

Data can be passed in as substitution_data or metadata. The difference is that the metadata is sent through webhook payloads and message events, while substitution_data is only used for generating the template. If there is a conflict between substitution_data and metadata, the substitution_data value takes precedence.

Both metadata and substitution_data can be set inside the top-level transmission object and inside each recipient. The recipient-level data takes precedence over the transmission-level data.

In the following transmissions example, the hierarchy for the value of city is as follows: San Francisco, Seattle, Baltimore, and New York.

The email would therefore render as: Hello, New York.

{
  "metadata": {
    "city": "San Francisco"
  },
  "substitution_data": {
    "city": "Seattle"
  },
  "recipients": [
    {
      "address": "wilma@flintstone.com",
      "metadata": {
        "city": "Baltimore"
      },
      "substitution_data": {
        "city": "New York"
      }
    }
  ],
  "content": {
    "from": "fred@flintstone.com",
    "subject": "Hello",
    "html": "Hello, {{city}}!"
  }
}

Reserved Variables

As a convenience, SparkPost also adds additional variables based on the transmission details. They are as follows:

  • address.name – Name from the recipient details.

  • email, email_id, and address.email – Email address from recipient details.

  • env_from – Return-Path address, sometimes called a "bounce address" or "envelope sender address"

  • return_path - Enterprise The return_path substitution variable is available to SparkPost Enterprise users.

Expressions

Expressions allow you to insert variables into your content.

An expression is a {{, some content, followed by a }}. Any whitespace inside the double curly braces is ignored. All of the following are equivalent:

HTML
Data
Hello 👋 Hello 👋 Hello 👋

Missing Variables

An empty string is substituted for variables that do not exist or have a value of null.

HTML
Data
* Jane * * Software Engineer *

Default Values

To avoid rendering an empty string, you can set default values using the or operator. In the following example, if name does not exist, then the expression will evaluate as Customer.

HTML
Data
Hello Customer

Nested Object Paths

You can reference variables that are nested inside arrays or objects using dot notation and square brackets. You can even use variables as part of the path like part is used in the example below.

HTML
Data
Street: Howard Street City: San Francisco Dynamic: Howard Street

HTML Escaping

By default, variables in the transmissions html and amp_html content are HTML escaped. However, in the text content, variables are not HTML escaped. To render a value unescaped in the html and amp_html content, wrap it in triple curly braces, {{{ value }}}.

HTML
Data
Escaped: &lt;b&gt;Hello, World&lt;&#x2F;b&gt; Unescaped: <b>Hello, World</b>

Statements

Statements allow you to implement logic into your templates. You can conditionally render content and loop through the data passed to the template. They have the same syntax as expressions but start with specific keywords such as if.

Unlike expressions, statements don't return a value. Therefore, if they are on their own line, they will not produce a blank line in the resulting output. In addition, any whitespace after the statement will also not rendered.

In the following example, the template renders without blank lines:

HTML
Data
Start of template Maryland End of template

if/else Statement

The if/else statement allows you to conditionally render some content if a specified condition is true or is not null. If the condition is false or null, another block of content may be rendered.

{{if signed_up}}
Welcome
{{end}}

You may optionally include then at the end of an if statement.

{{if signed_up then}}
Welcome
{{end}}

You can combine multiple if/else statements using elseif statements.

{{if signed_up}}
Welcome
{{elseif rejected_sign_up}}
We won't bug you
{{else}}
Please sign up
{{end}}

Relational and Logical Operators

You can use relational and logical operators for granular control of what content renders.

{{if not signed_up}}
Don't forget to sign up!
{{end}}

{{if age > 30}}
do something
{{else}}
do something else
{{end}}

{{if address.state == "MD"}}
do something
{{end}}

-- multi part conditionals
{{if age > 30 and address.state == "MD"}}
do something
{{end}}

The relational and logical operators are as follows:

Relational Operators

ExpressionDescription
x == yx is equal to y
x != yx is not equal to y
x < yx is less than y
x > yx is greater than y
x <= yx is less than or equal to y
x >= yx is greater than or equal to y

Logical Operators

Expression
and
or
not

The Length Operator

The length operator # gives the length of an array.

HTML
Data
Number of states: 2

Arithmetic Operators

Recognized arithmetic operators are as follows:

Expression
+
-
*
/
HTML
Data
Your discounted price is $10.

each Statement

You can use an each statement to iterate over an array. If the array is empty or the value is null, nothing is rendered.

Inside the loop, you can access the current value via the loop_var variable. The current index can be accessed through the loop_index variable.

The following example iterates over array of strings and print out the value of each string:

HTML
Data
You have a child named Rusty You have a child named Audrey

To iterate over an array of objects, the syntax is the same, but access to the nested fields of the object is done using dot notation:

HTML
Data
Your shopping cart has items in it: Item: Jacket, Price: 39.99 Item: Gloves, Price: 5

Nested Loops

When using nested loops, because you have multiple loop variables, you should access the values using loop_vars.<name of the array> (notice loop_vars is plural). The following example uses shopping_cart and a_nested_array:

HTML
Data
--- Item: Jacket Price: 39.99 This item has the following nested values: Nested value: v2 Nested value: v1 --- Item: Gloves Price: 5 This item has the following nested values: ---

Array Indexing

It is possible to access specific items within an array:

You have children named {{ children[1] }} and {{ children[2] }}.

Array indexing and dot notation may also be used together:

The first item in your shopping cart is {{ shopping_cart[1].name }}.

Links

Links are handled slightly differently than regular content. All of the following applies to both links in the html, amp_html and text content.

Links in Variables

The template language identifies links by the fact that they start with a protocol – https:// or http://. If the protocol is stored inside the variable, the link won't get recognized correctly. The means that it won't get tracked and expressions that are used in the link won't be escaped properly.

To use links that are entirely stored in variables processed correctly, render them as dynamic content.

Links in Loops and Conditionals

Links are extracted from the template based on the fact they start with a protocol as explained in Links in Variables. When links are templated inside of loops or conditionals, there should be clear whitespace delimiters such as new lines or space between the link and any templating keywords.

{{if host}}
https://{{{host}}}/
{{else}}
https://www.sparkpost.com/
{{end}}

Personalized Links

You can use expressions inside of links to customize them. Instead of HTML escaping the value, expressions will URL encode the value.

HTML
Data
Personalized link: <a href="https://company.com/dailydeals?user=john&offercode=Daily%20Deal%21">Go!</a>

URL Encoding

Just as with HTML escaping, you can disable URL encoding by using triple curly braces. This is useful when you have multiple pieces of the URL in one variable.

HTML
Data
<a href="https://www.company.com/groups">click me</a> <a href="http://www.company.com/groups/join?user=clark">Go</a>

Custom Link Attributes

The template language provides a few custom HTML attributes for attaching extra functionality to your links.

With the exception of data-msys-linkname, these custom attributes can also be used for links in the text part of a message using a double-square-bracket notation.

http://www.example.com[[data-msys-clicktrack="0"]]

Link Names

Link names are used to identify links in click events in both webhook payloads and message events. You can name links through the data-msys-linkname custom attribute. If this attribute is not specified the link name will be reported as Raw URL. For example:

<a href="http://www.example.com" data-msys-linkname="banner">Example</a>

Link names have a maximum length of 63 characters and are truncated if they exceed the limit.

Unsubscribe Links

You can configure a link in your content to generate a link_unsubscribe event when clicked using the data-msys-unsubscribe custom attribute. Click tracking is required for the generation of a link_unsubscribe event. For example:

<a href="http://www.example.com/unsub_handler?id=1234" data-msys-unsubscribe="1">Unsubscribe</a>

More information can be found here.

Per-link Disabling of Click Tracking

When click-tracking is enabled for a transmission, individual links can be skipped using the data-msys-clicktrack custom attribute. For example:

<a href="http://www.example.com/" data-msys-clicktrack="0">Click</a>

Custom Link Sub-Paths

It is possible to add a custom sub-path to a tracked URL using the data-msys-sublink custom attribute. For example:

<a href="http://www.example.com/" data-msys-sublink="custom_path">Click</a>

The tracked link generated will look like this:

http://<hostname>/f/custom_path/<encoded target url>

Macros

Macros are built-in functions. Each macro consists of a name followed by parentheses, which may enclose any arguments that the macro accepts.

render_dynamic_content()

This macro allows you to render substitution variables, stored inside the dynamic_html, dynamic_amp_html and dynamic_plain. It will execute all expressions and will track all links.

Learn more about dynamic content.

empty()

Given an array as an argument, the empty macro returns true if the array is empty or false if the array is not empty. This is useful for determining whether to include a header in a dynamically generated HTML table and blocking iteration of the table if it is empty.

HTML
Data
<table> <tr> <th>Name</th> <th>Price</th> </tr> <tr> <td>Jacket</td> <td>$39.99</td> </tr> <tr> <td>Gloves</td> <td>$5</td> </tr> </table>

render_snippet()

See this section.

Braces Macros

If you want opening or closing braces to appear in the content, you must escape them. The six macros for outputting braces are as follows:

MacroOutput
opening_single_curly(){
closing_single_curly()}
opening_double_curly(){{
closing_double_curly()}}
opening_triple_curly(){{{
closing_triple_curly()}}}
HTML
Data
Here is a curly: {{

Escaping curly braces in amp-mustache templates

AMP HTML email content may contain "amp-mustache" templates. Most double and triple curly brace expressions within amp-mustache templates are automatically escaped by SparkPost such that the amp-mustache expressions are preserved upon delivery to email clients.

There are certain expressions that SparkPost will not escape. These include:

  • Expressions that contain the keyword "sparkpost"

  • Expressions that are already a call to one of the curly escape macros: opening_single_curly(), closing_single_curly(), opening_double_curly(), closing_double_curly(), opening_triple_curly(), and closing_triple_curly()

  • Expressions that contain use of "loop_var"

  • The "else" and "end" control statements

In the following example, {{name}}, {{price}}, {{#cart_items}}, {{/cart_items}}, and {{^cart_items}} will be automatically escaped such that they are preserved as amp-mustache template expressions upon delivery of the email. On the other hand, {{sparkpost_name}} will be treated as a sparkpost templating expression since it contains the "sparkpost" keyword.

<amp-list src="https://ampbyexample.com/json/cart.json" layout="fixed-height" height="80">
    <template type="amp-mustache">
      <div id="cart">
        Cart items for {{sparkpost_name}}:<br>
        {{#cart_items}}
        <div class="cart-item">
            <span>{{name}}</span>
            <span>{{price}}</span>
          </div>
        {{/cart_items}}
        {{^cart_items}}
          There are no featured products available. Please check back again later.
        {{/cart_items}}
      </div>
    </template>
  </amp-list>

Dynamic Content

Dynamic content is content stored in a variable that contains expressions, statements, and links. Expressions inside of variables are not executed by default. Likewise, links are not tracked. To correctly render dynamic content, you must do the following:

  1. Add the dynamic content inside the dynamic_html object at the transmission-level substitution data.

  2. Use the render_dynamic_content() macro to render the dynamic_html variables.

Here is an example of dynamic content used correctly. Notice that the {{username}} is replaced with foo in the output.

HTML
Data
<body> <p>Insert a chunk of html:</p> <p><a href="http://www.example.com?q=foo">Click here</a></p> </body>

The dynamic content will be correctly rendered without HTML escaping, regardless of whether double or triple curly braces are used.

Plain text

To insert dynamic content into the text/plain part of a message, place the dynamic content into the transmission-level substitution variable dynamic_plain.

As with dynamic_html, dynamic_plain variables must be wrapped in the render_dynamic_content() macro when used in the template.

AMP HTML

You can also insert dynamic content into the text/x-amp-html part of a message using the transmission level dynamic_amp_html variable:

{
  "substitution_data" : {
    "dynamic_amp_html" : {
      "my_amp_html_chunk": "<amp-img width=\"30\" height=\"30\" src=\"https://www.example.com?u={{username}}\">"
    }
  }
}

As with dynamic_html and dynamic_plain, you must use the render_dynamic_content() macro to include your dynamic AMPHTML in the template:

Attempting to insert a chunk of amp html:
{{ render_dynamic_content(dynamic_amp_html.my_amp_html_chunk) }}

Complex Example

Finally, as a more complex example, render_dynamic_content() can also be used inside an each loop. This example only renders the offers available as listed in the offers array.

HTML
Data
<h3>Today's special offers</h3> <ul> <li><a href="http://t.com/offer/1?name=John">Premium-brand wirecutters</a></li> <li><a href="http://t.com/offer/3?name=John">Super-effective bug spray</a></li> </ul>

For AMP HTML, the same example could be laid out using the amp_html part

{
    "name": "John",
    "offers": [ "offer1", "offer3" ],
    "dynamic_html": {
        "offer1": "<a href=\"http://t.com/offer/1?name={{name}}\">Premium-brand wirecutters</a>",
        "offer2": "<a href=\"http://t.com/offer/2?name={{name}}\">Corks</a>",
        "offer3": "<a href=\"http://t.com/offer/3?name={{name}}\">Super-effective bug spray</a>"
    },
    "dynamic_amp_html": {
      "offer1": "<a href=\"http://t.com/offer/1?name={{name}}\">Premium-brand wirecutters</a>",
      "offer2": "<a href=\"http://t.com/offer/2?name={{name}}\">Corks</a>",
      "offer3": "<a href=\"http://t.com/offer/3?name={{name}}\">Super-effective bug spray</a>"
    },
     "content": {
        "html": "<p>Today's special offers</p><ul>\n{{each offers}}\n<li>{{render_dynamic_content(dynamic_html[loop_var])}}</li>\n{{end}}\n</ul>",
        "amp_html": "<!doctype html><html amp4email><head><meta charset=\"utf-8\"><script async src=\"https://cdn.ampproject.org/v0.js\"></script><style amp4email-boilerplate>body{visibility:hidden}</style></head><body><p>Today's special offers</p><ul>\n{{each offers}}\n<li>{{render_dynamic_content(dynamic_amp_html[loop_var])}}</li>\n{{end}}\n</ul></body></html>",
        "from": "test@example.com",
        "subject": "offers"
    }
  }
}

Snippets

Snippets are pieces of reusable content that are managed using the Snippets endpoint. Once a snippet is created, it can be imported into any html, amp_html, or text email content using the render_snippet macro call.

Snippets are similar to the dynamic_html, dynamic_amp_html, and dynamic_plain described in the previous section, with the key difference being snippets are created ahead of time rather than defined inline within a transmissions request.

For example, the /api/labs/snippets endpoint can be used to create a snippet with the ID ourfooter:

{
  "id" : "ourfooter",
  "content" : {
    "html" : "<footer><p>Our standard html footer content</p></footer>",
    "text" : "Our standard plain text footer content",
    "amp_html" : "<footer><p>Our standard amp html footer content</p></footer>"
  }
}

The snippet can then be imported into either plain text, html, or amp_html email content using the render_snippet macro call. The macro call will automatically use the appropriate content.html, content.amp_html, or content.text snippet value based on the type of content that the snippet is being inserted into. For example, if a transmission was injected with content.html of the form:

<html>
<p>Our body content</p>
{{ render_snippet( "ourfooter" ) }}
</html>

The resulting rendered HTML email content would look like:

<html>
<p>Our body content</p>
<footer><p>Our standard html footer content</p></footer>
</html>

The render_snippet macro takes a snippet ID as its only argument, which may come from substitution_data. The following example illustrates how one of two plain text snippets can be utilized, based on the recipient's substitution_data.

Two snippets are created:

{
  "id" : "banner_snippet_A",
  "content" : {
    "text" : "Banner A"
  }
}
{
  "id" : "banner_snippet_B",
  "content" : {
    "text" : "Banner B"
  }
}

Transmission with content.text:

The following banner depends on the "banner_id" substitution_data value
{{ render_snippet( banner_id ) }}

Where Recipient 1 has substitution_data:

{
  "banner_id" : "banner_snippet_A"
}

And Recipient 2 has substitution_data:

{
  "banner_id" : "banner_snippet_B"
}

Other notes on snippet usage:

  • Snippets themselves may contain substitution syntax. Though some restrictions apply: snippets cannot reference other snippets (render_snippet), nor can they utilize render_dynamic_content.

  • Snippets may contain links (which will be click tracked if click_tracking is enabled).

  • If a render_snippet call references a snippet which does not exist, the transmission will be discarded with a generation failure event.

  • A template may utilize render_snippet at most 5 times. If this limit is exceeded, the transmission will be discarded with a generation failure event.

  • Snippet UI can take up to 2 minutes to reflect the changes made.

Advanced

Substitutions in email_rfc822 Headers

When it is desirable to have substitutions in RFC2047 encoded headers which are folded, be sure that each line of the header is separately RFC2047 encoded. Otherwise, the server will not be able to decode the header to look for the template language syntax.

Correct:

Subject: =?gb2312?B?ztLE3M3Mz8Kyo8Gntviyu8nLye3M5c7SxNzNzM/CsqPBp7b4srvJy8ntzOU=?=
   =?gb2312?B?ztLE3M3Mz8Kyo8Gntvg=?= 

Incorrect:

Subject: =?gb2312?B?ztLE3M3Mz8Kyo8Gntviyu8nLye3M5c7SxNzNzM/CsqPBp7b4srvJy8ntzOU=
   ztLE3M3Mz8Kyo8Gntvg=?=

Encoding Rules

  • If after substitution, a text/plain, text/html, or text/x-amp-html part contains 8-bit data, then that part will be quoted-printable encoded before being placed back into the MIME structure. The Content-Type will be updated appropriately.

  • If after substitution, a header value contains 8-bit data, then the header value will be RFC2047 base64 encoded before being written back to the headers structure.