Skip to main content
Merge tags are placeholders you insert into a template so values can be filled in automatically at send time. In Lettr templates, merge tags use a structured template language that goes beyond simple field replacement. You can output variables, set defaults when data is missing, conditionally render sections, loop through arrays, and safely generate personalized links—all using a consistent brace-based syntax. This guide covers what you need to confidently use merge tags in production templates: how data is resolved, how to write expressions and logic, how link substitution behaves, and how to test and troubleshoot common issues.

Providing Data at Send Time

To populate merge tags, include a substitution_data object in your API request:
The substitution_data object can contain:
  • Strings: Simple text values
  • Numbers: Numeric values for calculations or display
  • Booleans: For conditional rendering
  • Objects: Nested data accessible via dot notation
  • Arrays: For looping through multiple items
All values in substitution_data are converted to strings when rendered. Numbers and booleans work in conditionals but output as text.

Understanding substitution_data vs metadata

Lettr supports two data objects you can include when sending emails: Use substitution_data for any values that should appear in your email content (names, order details, personalized URLs, etc.). Use metadata for tracking information you want to receive in webhook payloads (internal IDs, campaign codes, A/B test variants, etc.).

Template Variables

Merge tags read values from JSON data provided at send time. The template language supports two common data scopes: substitution data and metadata. When the same key exists in more than one place, values are resolved using precedence rules: recipient-level data takes priority over transmission-level data, and substitution data takes priority over metadata.

Key Naming Rules

Variable keys must contain only US-ASCII letters, digits, and underscores. Keys must not start with a digit.
Certain words are reserved by the template language and cannot be used as custom keys: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

Common Built-in Recipient Fields

In addition to your own keys, you can reference common recipient fields when they are present in your send context: Availability depends on what your system provides at send time.

Expressions: Inserting Values

An expression is anything inside double curly braces. Expressions output a value into your template. Whitespace inside the braces is ignored, so {{value}} and {{ value }} behave the same.

Basic Variable Output

Template
Data
Result

Missing Variables Become Empty Strings

If a variable does not exist or its value is null, it renders as an empty string.
Template
Data
Result

Default Values with or

To avoid empty output when data is missing, use or to provide a fallback value.
Template
Data
Result
Use or fallbacks anywhere a blank value would make the copy awkward—greetings and short labels are common examples.

Nested Object Paths

You can access nested fields using dot notation or bracket notation. Bracket access is useful for keys with special characters or for dynamic property access.
Template
Data
Result

Array Indexing

You can access individual array elements using bracket notation. Array indexes start at 1 (not zero).
Template
Data
Result

Escaping: Double vs Triple Braces

The escaping behavior depends on the content type:
  • HTML and AMP HTML content: Values inserted with {{ ... }} are HTML-escaped by default
  • Plain text content: Values are NOT HTML-escaped
If you intentionally want unescaped output in HTML content (for example, inserting trusted HTML), use triple braces {{{ ... }}}.
Template
Data
Result
Disabling HTML escaping without properly sanitizing the input may expose recipients to CSRF or XSS attacks. Only use triple braces with trusted content.

Statements: Conditionals and Loops

Statements also use brace syntax but start with keywords such as if and each. Statements control what gets rendered rather than outputting a direct value.
Statements on their own line will not produce a blank line in the output. Any whitespace after the statement also won’t render.

Conditional Blocks

An if block renders when its condition is true. You can include elseif branches and a final else, closing the block with end.
Template
Data
Result
You can also use the optional then keyword after the condition:
Template

Operators in Conditions

Conditions support relational comparisons and logical operators:
Template
Data

Array Length with #

Use the # prefix to get the length of an array.
Template
Data
Result

Arithmetic in Expressions

Basic arithmetic operators are supported: +, -, *, and /.
Template
Data
Result

Looping with each

Use each to iterate over an array. Inside the loop, loop_var holds the current item and loop_index holds the current index. If the array is empty or null, nothing is rendered.
If you’re building templates in the visual editor and don’t need code-level loop control, consider using Loop Blocks instead. Loop Blocks provide the same repeating functionality through a drag-and-drop interface.

Array of Strings

Template
Data
Result

Array of Objects

Template
Data
Result

Nested Loops with loop_vars

In nested loops, use loop_vars.<arrayName> to reference the current item of a specific loop and avoid ambiguity.
Template

Links require extra care because template systems often extract and rewrite URLs for tracking. To ensure a link is recognized correctly, the URL should start with a literal protocol (http:// or https://) in the template itself—not embedded only inside a variable.
If the protocol (e.g., https://) is stored inside a variable rather than written literally in the template, the link won’t be recognized correctly and won’t be tracked.

Query Strings and Encoding

When expressions are used inside link URLs, inserted values are URL-encoded by default (not HTML-escaped). This is usually what you want for query string parameters.
Template
Data
Result
No spaces are allowed when an expression is used inside a query string. Keep expression formatting tight.

Disabling URL Encoding

If you need to insert a URL segment that is already correctly encoded, use triple braces inside the URL to disable URL encoding.
Template
Data
Result
Disabling URL encoding without handling encoding in your application may expose recipients to CSRF or XSS attacks.
When links appear inside if blocks or each loops, keep clean whitespace boundaries (newlines or clear spacing) around statement tags so URL extraction remains reliable.
Template
You can add special attributes to links to control tracking behavior and add metadata. Add a custom name to a link for easier identification in analytics:
Link names are limited to 63 bytes and will be automatically truncated if longer.
Mark a link as an unsubscribe action to generate proper unsubscribe events:
Click tracking must be enabled for unsubscribe events to be generated.

Disable Click Tracking

Prevent a specific link from being tracked:

Custom Sub-Paths

Add a custom path segment to the tracked link URL:
The generated tracked link will include your custom path: http://<tracking-domain>/f/custom_path/<encoded-target-url> For plain text email content, use double-bracket notation to add link attributes:

Macros: Built-in Helpers

Macros look like function calls: a name followed by parentheses. They provide helpers for common template patterns that are awkward to express with plain expressions.

empty(array)

Returns true when an array is empty (or effectively empty). Useful for skipping headings, tables, or repeated sections when there is nothing to show.
Template
Data
Result

render_dynamic_content()

Expressions inside a variable’s string value are not evaluated automatically. If you store a chunk of template-ready HTML (containing merge tags and links) inside a variable, render it using render_dynamic_content(). This macro executes all expressions and tracks all links within the dynamic content. Use this macro with content stored in these special variables:
  • dynamic_html - for HTML content
  • dynamic_amp_html - for AMP HTML content
  • dynamic_plain - for plain text content
The dynamic content will be correctly rendered without HTML escaping, regardless of whether double or triple curly braces are used.

HTML Dynamic Content

Template
Data
Result

AMP HTML Dynamic Content

Data

Dynamic Content with Loops

You can combine render_dynamic_content() with loops to render multiple dynamic blocks:
Template
Data
Result

render_snippet()

Renders reusable content blocks (snippets) that are managed separately from your templates. The system automatically selects the appropriate content type (HTML, plain text, or AMP HTML) based on the context.
Template
You can also use a variable to specify the snippet ID:
Template
Snippet Limitations:
  • Snippets cannot reference other snippets (no nested render_snippet calls)
  • Snippets cannot use render_dynamic_content
  • A template may use render_snippet at most 5 times
  • If a render_snippet call references a non-existent snippet, the message will fail with a generation error

Outputting Literal Braces

If you need to output literal curly braces (for example, when documenting syntax or embedding another templating system), use the brace-output macros:

Testing Merge Tags

A reliable merge-tag workflow treats templates and data as a pair.
1

Define your data contract

Decide which keys must exist, which are optional, and which values need defaults.
2

Add fallbacks for optional fields

Use or fallbacks anywhere a blank value would make the copy awkward.
3

Test with complete data

Validate the template renders correctly with all fields populated.
4

Test with sparse data

Validate with intentionally missing fields to expose where you need defaults or conditional blocks.
5

Validate links

Click links in test messages, especially when merge tags appear in query strings.
Remember that URL values are encoded by default inside links, and keep expression formatting tight (avoid unnecessary spaces) in query strings.

Troubleshooting

The most common cause is a syntax issue: typos, missing braces, or mismatched quotes. It can also happen when using a reserved keyword as a key. Reinsert the tag carefully and verify the key naming rules.
Confirm whether the input data is missing or null. Decide whether a default via or is sufficient, or whether an if block should hide the surrounding sentence entirely.

Templates Introduction

Get started with email templates

Topol Email Editor

Design emails with the visual editor

Loop Blocks

Visual drag-and-drop repeating content

Test Emails

Preview templates before sending

Template Versions

Manage template versions

Campaign Merge Tags

{{unsubscribe_link}} and {{webversion_link}} for marketing campaigns