Providing Data at Send Time
To populate merge tags, include asubstitution_data object in your API request:
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:| Field | Purpose | Used in Templates | Available in Webhooks |
|---|---|---|---|
substitution_data | Populate merge tags | Yes | No |
metadata | Custom tracking data | No | Yes |
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.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:| Field | Description |
|---|---|
address.name | Recipient’s display name |
address.email | Recipient’s email address |
email or email_id | Shorthand for recipient email |
env_from | Return-Path/bounce address |
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 isnull, 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
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 at1 (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
{{{ ... }}}.
Template
Data
Result
Statements: Conditionals and Loops
Statements also use brace syntax but start with keywords such asif 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
Anif block renders when its condition is true. You can include elseif branches and a final else, closing the block with end.
Template
Data
Result
then keyword after the condition:
Template
Operators in Conditions
Conditions support relational comparisons and logical operators:| Operator | Description |
|---|---|
== | Equal to |
!= | Not equal to |
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
and | Logical AND |
or | Logical OR |
not | Logical NOT |
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.
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 and URLs
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.
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
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
Links Inside Statements
When links appear insideif blocks or each loops, keep clean whitespace boundaries (newlines or clear spacing) around statement tags so URL extraction remains reliable.
Template
Link Attributes
You can add special attributes to links to control tracking behavior and add metadata.Link Names
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.
Unsubscribe Links
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:http://<tracking-domain>/f/custom_path/<encoded-target-url>
Text Link Attributes
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 contentdynamic_amp_html- for AMP HTML contentdynamic_plain- for plain text content
HTML Dynamic Content
Template
Data
Result
AMP HTML Dynamic Content
Data
Dynamic Content with Loops
You can combinerender_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
Template
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:| Macro | Output |
|---|---|
{{opening_single_curly()}} | { |
{{closing_single_curly()}} | } |
{{opening_double_curly()}} | {{ |
{{closing_double_curly()}} | }} |
{{opening_triple_curly()}} | {{{ |
{{closing_triple_curly()}} | }}} |
Testing Merge Tags
A reliable merge-tag workflow treats templates and data as a pair.Define your data contract
Decide which keys must exist, which are optional, and which values need defaults.
Add fallbacks for optional fields
Use
or fallbacks anywhere a blank value would make the copy awkward.Test with sparse data
Validate with intentionally missing fields to expose where you need defaults or conditional blocks.
Troubleshooting
Merge tag appears literally in output
Merge tag appears literally in output
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.
Merge tag resolves to empty value
Merge tag resolves to empty value
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.Link breaks or behaves inconsistently
Link breaks or behaves inconsistently
Confirm the URL starts with a literal
http:// or https:// in the template. Verify the merge tag is placed in the correct field (link destination for URL-producing values). When links appear inside loops or conditionals, keep statement boundaries clean so link extraction remains reliable.Link is not being tracked
Link is not being tracked
If the protocol is stored inside a variable rather than written literally in the template, the link won’t be recognized for tracking. Ensure the protocol appears literally in your template code.