Templates and merge tags
Compose reusable email bodies and resolve per-recipient values with merge tags.
A template is a reusable body with {{merge-tag}} placeholders. Which tags are
available depends on the template's category — an auth template gets
different tags than an audience campaign.
Fetch the valid tags for a category rather than hardcoding them:
curl https://api.console.buildbase.app/api/emails/merge-tags/audience \
-H "Authorization: $BUILDBASE_TOKEN"[
{ "title": "First Name", "mergeTag": "{{firstName}}", "sample": "Alice" },
{
"title": "Unsubscribe",
"mergeTag": "{{unsubscribe}}",
"sample": "https://example.com/unsubscribeLink"
}
]Before you start
Templates are created and edited in the BuildBase console under Email → Templates. The API manages them but the editor is where the body is authored.
Categories
8 categories exist, and the category determines the merge-tag set:
| Category | Used for |
|---|---|
auth | Sign-in, verification, password reset |
user | Lifecycle mail addressed to one user |
contact | Replies to contact-form submissions |
audience | Campaigns to an audience list |
beta-users | Beta programme mail |
notification | Bodies rendered by bb.notification.send() |
default | System defaults shipped with BuildBase |
custom | Anything outside the above |
A system template also carries an explicit tag list keyed by template ID, which
takes priority over the category. Requesting merge tags with a templateId
returns that template's exact event tags plus the shared base set.
Two tags every audience email needs
| Tag | Resolves to |
|---|---|
{{unsubscribe}} | <your-app>/unsubscribe-email-group?token=… |
{{manage-preference}} | <your-app>/manage-email-preference?token=… |
Both mint a per-recipient token, so the link identifies the recipient without a login. They resolve at send time — a template previewed in the console shows the sample value, not a live link.
These two are the exception to the substitution rules below: the main merge pass deliberately leaves them untouched so a later pass can attach the recipient's token. That is why they survive even though no value for them exists in the merge data.
Checking tag usage
/api/emails/merge-tags/usage reports which tags are actually referenced. The
route is registered before /merge-tags/:category so that usage is not
parsed as a category name — a detail worth knowing if you add categories.
Behavior
| Behavior | Detail |
|---|---|
| Unknown tag | Replaced with an empty string. It is not left visible, so a typo fails silently |
| Resolution time | Send time, per recipient — not at template save |
| Preview | Uses the sample value from the tag definition |
| Category mismatch | A tag valid in one category resolves to nothing in another. Fetch per category rather than reusing a list |
| Nested data | Merge data is flattened, so {{user.name}} addresses a nested field |
| System templates | Can be reset to their shipped default, duplicated, or claimed for ownership |
Warning
Falsy values render as empty. Substitution uses value || '', so a merge
value of 0, false, or an empty string produces nothing — indistinguishable
from a misspelled tag. A body reading You have {{credits}} credits left
renders as "You have credits left" at zero, which is exactly when you most
want to send it.
Pre-format such values into strings in the sending data ("0") rather than passing raw numbers.
Because a typo and a legitimately empty value look identical in the output, send yourself a test before a campaign goes to an audience.
One convenience worth knowing: markdown renderers percent-encode link
destinations, so a tag used as a URL — [Sign in]({{link}}) — would otherwise
arrive as %7B%7Blink%7D%7D. That is normalized back before substitution, so
merge tags work inside link targets.
Next Steps
- Sending domains — verify a domain before sending.
- Campaigns overview — the send pipeline.