Template Language

The SparkPost API provides a powerful handlebars-style template language that you can use in the email subject, headers, text, and 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.

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

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


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 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:

Hello 👋 Hello 👋 Hello 👋

Missing Variables

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

* 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.

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.

Street: Howard Street City: San Francisco Dynamic: Howard Street

HTML Escaping

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

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


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:

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}}

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

{{if signed_up then}}

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

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

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!

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

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

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

The relational and logical operators are as follows:

Relational Operators

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


The Length Operator

The length operator # gives the length of an array.

Number of states: 2

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:

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:

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:

--- 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 are handled slightly differently than regular content. All of the following applies to both links in the 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.

Personalized Links

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

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.

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

These custom HTML can also be used for links in the text part of a message using a double-square-bracket notation.


Link Names

Link names are used to identify links in click events in both webhook playloads 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 an link_unsubscribe event when clicked using the data-msys-unsubscribe custom attribute. 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 are built-in functions. Each macro consists of a name followed by parentheses, which may enclose any arguments that the macro accepts.


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

Learn more about dynamic content.


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.

<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>

Braces Macros

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

Here is a curly: {{

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.

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

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.

<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>


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.


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


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

Encoding Rules

  • If after substitution, a text/plain or text/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.