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.
Your app's name and logo
Two tags come from the hosted sign-in page, so an email names the app the way the page does.
| Tag | Value |
|---|---|
{{appName}} | In an email sent during an app's sign-in, that app's own display name when it has one. Otherwise the App Name merge tag, when you set one, then the sign-in page's Name, then your organization's name. Every organization started with its own name in App Name, and that counts as not set |
{{logoUrl}} | In an email sent during an app's sign-in, that app's logo when it has one. Otherwise the sign-in page's logo. An absolute URL, and empty when there is no logo |
The shipped templates do not draw a logo, because each organization keeps its own copy of every template and changing ours would not reach yours. To show it, add it to the templates you send:
<img src="{{logoUrl}}" alt="{{appName}}" height="40" />The template editor's tag list shows both with the values a send would use. A tag you create with the same key wins over the page's.
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 |
| HTML escaping | In the HTML part a value is text: <, >, & and quotes are escaped, so a value cannot add markup |
| 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.