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:
Sophisticated logic with full conditionals and logical operators.
Looping over arrays.
Reference nested object paths.
Default values if substitution data is absent.
Completely dynamic content.
Execution of built-in macros.
Automatic HTML escaping.
Automatic encoding of UTF-8 variables in email headers.
The template language can only be used through the API, not via SMTP
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.
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}}!"
}
}
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:
An empty string is substituted for variables that do not exist or have a value of null
.
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
.
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.
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 }}}
.
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:
if/else
StatementThe 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}}
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
Expression | Description |
---|---|
x == y | x is equal to y |
x != y | x is not equal to y |
x < y | x is less than y |
x > y | x is greater than y |
x <= y | x is less than or equal to y |
x >= y | x 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.
Recognized arithmetic operators are as follows:
Expression |
---|
+ |
- |
* |
/ |
each
StatementYou 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:
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:
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
:
Array indexes start at 1
. i.e. The first value in an array named items
is items[1]
.
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
, amp_html
and text
content.
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 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}}
You can use expressions inside of links to customize them. Instead of HTML escaping the value, expressions will URL encode the value.
No spaces are allowed when an expression is used inside a query string.
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.
Disabling URL encoding of variables can lead to broken links – be careful with this!
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 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.
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.
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>
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>
SparkPost Enterprise only: An example of how to use data-msys-sublink to support iOS Universal Links can be found here.
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.
render_snippet()
See this section.
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:
Macro | Output |
---|---|
opening_single_curly() | { |
closing_single_curly() | } |
opening_double_curly() | {{ |
closing_double_curly() | }} |
opening_triple_curly() | {{{ |
closing_triple_curly() | }}} |
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 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:
Add the dynamic content inside the dynamic_html
object at the transmission-level substitution data.
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.
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.
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 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.
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=?=
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.