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.

Two tags come from the hosted sign-in page, so an email names the app the way the page does.

TagValue
{{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

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
HTML escapingIn the HTML part a value is text: <, >, & and quotes are escaped, so a value cannot add markup
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