BuildBaseBuildBase

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:

CategoryUsed for
authSign-in, verification, password reset
userLifecycle mail addressed to one user
contactReplies to contact-form submissions
audienceCampaigns to an audience list
beta-usersBeta programme mail
notificationBodies rendered by bb.notification.send()
defaultSystem defaults shipped with BuildBase
customAnything 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

TagResolves 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

BehaviorDetail
Unknown tagReplaced with an empty string. It is not left visible, so a typo fails silently
Resolution timeSend time, per recipient — not at template save
PreviewUses the sample value from the tag definition
Category mismatchA tag valid in one category resolves to nothing in another. Fetch per category rather than reusing a list
Nested dataMerge data is flattened, so {{user.name}} addresses a nested field
System templatesCan 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