Merge Codes (Tags)¶
Merge codes are placeholders in your content that get replaced with dynamic values when the content is rendered. They allow you to personalize emails, SMS messages, web pages, and forms with recipient-specific data, event details, dates, and more.
Syntax¶
A merge code follows this pattern:
$$type.property$$
For example:
$$recipient.email$$— the recipient's email address$$event.name$$— the event name$$date.shortdate$$— today's date in short format
Alternative delimiters¶
Curly braces also work: {{recipient.email}}. Both forms are equivalent.
Available tag types¶
| Type | Description | Example properties |
|---|---|---|
recipient |
Recipient fields | email and/or mobilenumber (for SMS recipients), plus any custom recipient properties |
message |
Mail or SMS message | name, subject, description, fromname, fromemail |
publication |
Publication/list | name |
date |
Current date | shortdate, longdate, custom |
event |
Event details | name, description, start_date, end_date, date, registration_opens_date, registration_closes_date, spots_left, location_address, location_details |
formresponse |
Form response data | Any form field name, status, email, ticket_participated_qr_code_url |
share |
Social sharing | public_url |
survey |
Survey | name |
webpage |
Web page | name |
recipientpage |
Recipient page | name, gdpr_information, responsible_person_name, responsible_person_email |
multioptin |
Multi opt-in | name |
automatedflow |
Automated flow | name |
automatedflow_participant |
Flow participant | id, primaryemail, plus any participant property |
system_mail |
System mail | name, subject, fromname, fromemail |
mail_content |
Mail content | preheader, subject |
system |
System information | version |
custom |
Custom variables | Any key provided by the caller |
Formatting values¶
Format verb (recommended)¶
Use the |format() verb to control how dates and numbers are displayed:
$$event.start_date|format(yyyy-MM-dd)$$ → 2026-04-25
$$event.start_date|format(dd MMMM yyyy)$$ → 25 April 2026
$$event.start_date|format(HH:mm)$$ → 09:00
$$date.custom|format(yyyy-MM-dd HH:mm:ss)$$ → 2026-07-06 14:30:00
Legacy format syntax¶
The older parenthesis syntax is still supported for backward compatibility:
$$event.start_date(yyyy-MM-dd)$$
$$date.custom(yyyy-MM-dd HH:mm:ss)$$
Both forms produce identical output. The parenthesis syntax is internally converted to a format verb.
Common date formats¶
| Format string | Example output | Description |
|---|---|---|
d |
7/6/2026 |
Short date |
D |
Monday, July 6, 2026 |
Long date |
g |
7/6/2026 2:30 PM |
General (short date + short time) |
G |
7/6/2026 2:30:00 PM |
General (short date + long time) |
t |
2:30 PM |
Short time |
T |
2:30:00 PM |
Long time |
yyyy-MM-dd |
2026-07-06 |
ISO date |
yyyy-MM-dd HH:mm |
2026-07-06 14:30 |
ISO date + 24h time |
dd MMMM yyyy |
06 July 2026 |
Day + full month name + year |
MMMM |
July |
Full month name |
dddd |
Monday |
Full day name |
For the complete reference, see Standard date and time format strings and Custom date and time format strings.
Common number formats¶
| Format string | Input | Example output | Description |
|---|---|---|---|
N |
1234.5 |
1,234.50 |
Number with group separator and 2 decimals |
N0 |
1234.5 |
1,235 |
Number with group separator, no decimals |
N3 |
1234.5 |
1,234.500 |
Number with 3 decimals |
C |
1234.5 |
$1,234.50 |
Currency |
P |
0.85 |
85.00% |
Percentage |
F2 |
1234.5 |
1234.50 |
Fixed-point, 2 decimals |
For the complete reference, see Standard numeric format strings and Custom numeric format strings.
Culture-aware formatting
On public pages, date and number formatting follows the viewer's language: a culture query-string parameter if present, then the language cookie, then the browser language (Accept-Language header). The same merge code produces different output depending on the viewer's locale. For example, $$event.start_date|format(g)$$ shows 7/6/2026 2:30 PM for English (US) viewers and 2026-07-06 14:30 for Swedish viewers.
A mail or SMS has no viewer when it is sent, so it is formatted in the sending server's culture.
Default formats¶
When no format is specified, these defaults apply:
| Value type | Default format |
|---|---|
| DateTime | g (general short) |
| decimal, double, float | N (number with group separators) |
| int, long | No format (plain number) |
| string | No formatting |
Resolvers may specify their own default format, which takes precedence over the table above. For example, $$event.start_date$$ defaults to g (general short date/time) even without an explicit format verb.
Numeric recipient properties¶
Recipient properties of a numeric type support the full range of format strings:
$$recipient.balance|format(N2)$$ → 1,234.50
$$recipient.balance|format(C)$$ → $1,234.50
$$recipient.points|format(N0)$$ → 1,235
With no format verb, a numeric recipient property renders as the plain stored number — 1234.5, not 1,234.50. Group separators and fixed decimal places are opt-in via |format().
Text properties cannot be number-formatted
Formatting is applied to the stored value's actual type. If a numeric-looking value is stored as text on the recipient, |format(N2) has nothing to work with and is silently ignored — the value renders unchanged. If a format verb appears to do nothing, check the recipient property's configured type first.
Verbs (transforms)¶
Verbs transform the resolved value. Chain multiple verbs with |:
$$recipient.firstname|toupper$$ → BRUCE
$$recipient.firstname|tolower$$ → bruce
$$recipient.firstname|capitalize$$ → Bruce
$$recipient.firstname|trim$$ → (whitespace removed)
$$recipient.firstname|toupper|trim$$ → BRUCE
$$event.start_date|format(yyyy-MM-dd)$$ → 2026-04-25
| Verb | Description |
|---|---|
toupper |
Convert to uppercase |
tolower |
Convert to lowercase |
capitalize |
First letter uppercase, rest lowercase |
trim |
Remove leading and trailing whitespace |
format(x) |
Apply a format string (see Formatting above) |
urlencode |
Force URL encoding (override default) |
htmlencode |
Force HTML encoding (override default) |
noencode |
Disable encoding (override default) |
collapse / collapse(chars) |
Remove the surrounding whitespace when the value is blank (see below) |
Verbs are applied left-to-right after the value is resolved.
An unrecognised verb leaves the whole merge code unresolved, so a misspelt verb shows up in a test send rather than being silently dropped. A format string that does not fit the value is ignored, and the value renders unformatted. A property name cannot contain |, because | starts the verb list.
Removing the gap left by a blank value¶
A merge code that resolves to nothing still leaves the space around it:
Hi $$recipient.firstname$$! → "Hi !" for a recipient with no first name
The collapse verb removes that gap:
Hi $$recipient.firstname|collapse$$! → "Hi!" when blank
→ "Hi Bruce!" when set
collapse does nothing at all when the value is present, so it is safe to add to any merge code where a blank value would look wrong. It applies both when the value is blank and when the property is missing entirely.
Collapsing punctuation too¶
Whitespace is always collapsed. To also remove punctuation that would be left stranded, list the characters as an argument:
Dear $$recipient.firstname|collapse(,)$$, welcome → "Dear welcome"
The argument is a set of characters, not a list — spaces inside it are ignored, since whitespace always collapses. These are all equivalent:
$$recipient.firstname|collapse(,.)$$
$$recipient.firstname|collapse(, .)$$
$$recipient.firstname|collapse(. ,)$$
Only characters directly touching the merge code are removed, and a whole run of them is taken — Hi $$recipient.firstname|collapse(!)$$!!! renders Hi. Punctuation elsewhere in the sentence is untouched.
What collapse does and does not fix¶
| Content | Blank value renders |
|---|---|
Hi $$recipient.firstname\|collapse$$! |
Hi! |
Hi $$recipient.firstname\|collapse$$ there |
Hi there (one space) |
| A collapsed merge code alone on its own line | the surrounding line break is kept |
Dear $$recipient.firstname\|collapse(,)$$, welcome |
Dear welcome |
A question mark cannot be listed
? is reserved for the ?? fallback delimiter, so collapse(?) cannot be parsed and the merge code will render as raw text in your content. Use ! or ., or rephrase the sentence. All other punctuation works.
Collapse and fallbacks are alternatives
If a merge code has both a fallback and collapse, the fallback wins — it produces a value, so there is no gap left to collapse. Use ?? when you want substitute text, and collapse when you want the sentence to close up instead.
Collapse also cannot rescue a sentence that depends on the value for its meaning. Your order $$recipient.ordernumber|collapse$$ has shipped reads as Your order has shipped — grammatical, but missing the information. Use a fallback there instead.
Fallback values¶
Use ?? to provide a default value when a merge code can't be resolved:
$$recipient.firstname??Guest$$ → "Guest" if firstname is not available
$$recipient.company??$$ → empty string if company is not available
$$recipient.name|toupper??VALUED CUSTOMER$$ → "VALUED CUSTOMER" if name is not available
The fallback is used when:
- The tag type has no resolver (e.g., unknown type)
- The resolver returns no value for the property (e.g., recipient has no value for that field)
- The resolved value is blank — an empty string, or a string containing only whitespace
When a value IS resolved, the fallback is ignored and the resolved value is used normally (including any verbs).
A merge code that cannot be resolved here and has no fallback is left in the content as written — for example $$recipient.email$$ on a page that has no recipient.
Blank values count as missing¶
This matters more than it sounds. Imported recipient data stores "no value" as an empty string at least as often as it stores a true null — a CSV column present but empty, a spreadsheet cell with a stray space, an API field sent as "". From inside your content these are indistinguishable from a missing field, and all of them leave the same hole in the sentence. So all of them trigger the fallback:
$$recipient.firstname??Guest$$
| Stored value | Result |
|---|---|
NULL / field absent |
Guest |
"" |
Guest |
" " (whitespace only) |
Guest |
"bruce" |
bruce |
Numbers and booleans are never blank
Only text values are tested for blankness. A numeric 0 and a false are real values, not missing ones — $$recipient.balance??N/A$$ renders 0 for a recipient who owes nothing, never N/A. This is deliberate: treating zero as missing is a common templating-engine trap that silently corrupts financial and count-based content.
A blank value with no fallback still renders as blank
Blankness only triggers the fallback. If a merge code has no ?? fallback and the value is blank, the merge code renders as an empty string — it does not fall back to showing the raw $$recipient.firstname$$ text in your content.
Encoding behavior¶
Merge code values are automatically encoded based on where the tag appears in the content. This prevents XSS vulnerabilities and ensures URLs work correctly.
| Context | Encoding applied | Example |
|---|---|---|
| HTML body text | HTML encoding | & → &, < → < |
URL query parameter (?key=$$tag$$) |
URL encoding | @ → %40, & → %26, space → %20 |
URL path (/path/$$tag$$/page) |
URL path encoding | spaces → %20 |
HTML attribute (alt="$$tag$$") |
HTML encoding | Same as body text |
| Plain text (SMS, text part of a mail) | No encoding, except inside a link | Value used as-is; in https://…?q=$$tag$$ the value is URL-encoded as above |
You can override the automatic encoding with verbs:
$$recipient.name|noencode$$ → No encoding applied regardless of context
$$recipient.name|urlencode$$ → Force URL encoding even in body text
$$recipient.name|htmlencode$$ → Force HTML encoding even in plain text
noencode disables XSS protection for that tag
Context encoding is what prevents a recipient-supplied value from breaking out of an attribute or injecting script. With noencode, a value such as " onmouseover="alert(1) or javascript:... in an href is emitted verbatim into the page.
Only use noencode on properties that are system-generated or validated on import — never on free-text fields, and never on anything a recipient can set themselves through a form.
Values that are already encoded¶
Store values raw: the engine encodes each one for the place its merge code sits. Some imports store values already encoded for the link they will be used in, such as Wayne%20Enterprises. Encoding those a second time would break the link, so a value that is already validly encoded for its position is inserted as-is:
| Stored value | Context | Rendered output |
|---|---|---|
Wayne Enterprises |
URL query parameter | Wayne%20Enterprises |
Wayne%20Enterprises |
URL query parameter | Wayne%20Enterprises (kept) |
Ben & Jerry's |
HTML body text | Ben & Jerry's (kept, shows as Ben & Jerry's) |
Wayne%20Enterprises |
Plain text / SMS | Wayne%20Enterprises (plain text is never encoded) |
The rules:
- A value is kept only when it is a complete, valid encoding for its position, with nothing left that encoding would change. Anything else is encoded normally, so this can never let a value escape its position.
- Fallback values are checked the same way.
+for a space is recognised only alongside a%escape:Wayne+Enterprises%2Cis kept, with the+written as%20. A value whose only encoding is+(Wayne+Enterprises) is treated as raw text and encoded, because a literal+, as inC++, is common.|urlencodeand|htmlencodealways encode, so use them when a value really is the literal text%20or&. Without them such a value shows decoded, as a space or&.- Each tenant and merge code that relies on this is logged once, so support can see which content depends on pre-encoded data.
Storing raw values is still the better choice: a raw value works in body text, links and SMS alike, while a pre-encoded one shows its %20 as text in an SMS.
QR codes and barcodes¶
Merge codes inside barcode data= query parameters are automatically URL-encoded. Both plain and URL-encoded tag syntax work:
<!-- Plain syntax -->
<img src="/b/bc/qr/?data=$$recipient.email$$&vm=1&width=200" />
<!-- URL-encoded syntax (used by the editor) -->
<img src="/b/bc/qr/?data=%24%24recipient.email%24%24&vm=1&width=200" />
Both produce the same result: the resolved email address is URL-encoded in the data= parameter.
Caveats¶
Fallback values in URLs: A fallback is encoded for its position like any resolved value, so $$recipient.name??A b&c$$ in a query parameter renders A%20b%26c. The editor stores a & in an attribute as &; the fallback is decoded before it is encoded, so it is not encoded twice.
Special links are not merge codes: Some tags that look like merge codes (e.g., $$publication.unsubscribe$$, $$message.viewmailhtmlpart$$) are actually special link placeholders handled by a separate system. They are preserved as-is during merge code resolution and replaced later in the pipeline. Do not use formatting or verbs on special links — they will not work.
Nested merge codes: Some merge codes resolve to text that itself contains merge codes. For example, a preheader ($$mail_content.preheader$$) might contain Hello $$recipient.firstname$$!. The engine automatically expands these nested merge codes in a second pass — you don't need to do anything special. This only goes two levels deep to prevent infinite loops.
Two things are never expanded in that second pass. Answers a visitor typed into a form are inserted as text, so a visitor cannot pull other data into a page or mail by typing a merge code. And urlencode / htmlencode inside a resolved value are ignored, so only the content author decides how a value is encoded.
Subjects are one line: if the merge codes in a mail subject resolve to text containing line breaks, the lines are joined with ,.