Email templates
A template is a subject line and an email body that you save once and send many times. You write the parts that change as {{ variable }} placeholders, publish the template, and then send it by slug instead of pasting the same HTML into every API call. A template belongs to one workspace.
You can author and manage templates from the dashboard under Email → Templates, from the API at /v1/email/templates, from the terminal with the bird CLI, or through the MCP server when an agent does the authoring. The SDKs do not ship template methods. Sending a published template happens through the regular send endpoint.
What's in a template
Every template has two names, and they do different jobs:
- slug is the name you send the template by, for example welcome-email. You choose it when you create the template and it cannot be changed afterwards. A slug can contain lowercase letters, numbers, hyphens and underscores, has to start and end with a letter or a number, and can be up to 63 characters long. Two prefixes are off limits: bird_, which is reserved for our built-in templates, and emt_, which is the format used for template IDs. The dashboard calls this field Alias.
- name is a free-text display label. It defaults to the slug, and you can change it at any time. Nothing resolves through the name, so renaming a template for display never breaks a send.
Alongside those, a template has a permanent emt_ ID, which is fixed for its whole life. It also has a category, either marketing or transactional, and an authoring source: html, finished markup you provide, optionally personalized with Liquid. The category and the source are both fixed when you create the template.
We provide a catalog of built-in templates, and their slugs all start with bird_. A built-in template belongs to no workspace, cannot be edited, and is always sendable as it is. Copy one into your workspace to make it yours, and it becomes an ordinary template you can edit. The copy arrives as an unpublished draft that inherits the original's category, source, and language settings, so publish it before you send it.
Drafts and published versions
Every template has exactly one draft, which is the working copy you edit. It also has any number of published versions, each one numbered (1, 2, 3, and so on) and never changed again once it exists. Editing changes the draft in place. Publishing takes a snapshot of the current draft, turns it into the next numbered version, and makes that version the one sends use. The draft itself stays editable, so you can carry on working on the next one.
The rule that matters for sending is this: a send always uses the template's published version, and a draft is never sent on its own. You can keep editing the draft while a stable version keeps going out, then publish when the change is ready. Publishing a new version changes what later sends render. A send that has already been accepted is not affected by a publish that happens afterwards.

There are two more things you can do with versions. Discard the draft's changes to reset the draft back to whatever is currently published. Or roll back to make an earlier published version the one that sends use again. You can only roll back to a version that was published, never to the draft itself. Rolling back replaces the draft with that version's content, so anything unsaved in the draft is lost, and further editing starts from the version you rolled back to. A rollback does not create a new version.
Saving is guarded by a revision number. Send the revision you last read for the language you are saving. If someone else changed that language in the meantime, the save is refused as a conflict instead of overwriting their work. Leave revision out to save unconditionally. Publishing and rolling back use the draft's own revision the same way.
Content in more than one language
A template holds content in up to 25 languages, each with its own subject and body, tagged with a BCP-47 code such as en or pt-BR. One language is the template's default. Publishing a template publishes every language it holds at the same time. There is no way to publish one language on its own, so all of them have to be finished first. Every language needs a subject and a body, and the template's default language has to be one of the languages you have filled in. If any of that is missing, nothing is published, and the error tells you what is missing in each language so you can fix it all in one pass. You do not have to finish every language up front: publish the ones that are ready, and add the rest later.
A language needs an HTML body. You can leave its text out: publishing then creates a plain-text alternative from the HTML automatically, so you get both parts without writing the second one yourself.
Two settings cover a send that does not name a language the template has content for, and they guard against different mistakes:
| Setting | What it controls |
|---|---|
| on_missing_language | What happens when a send asks for a language the template does not have. fallback, the default, serves the closest match: it tries a broader form of the same language first, so a request for pt-BR can be served by a stored pt, and falls back to the template's default language after that. fail rejects the send instead, for content where sending the wrong language is worse than not sending at all. |
| language_source_required | Whether a send has to name a language at all. This is off by default, so a send that names none gets the default language. Turn it on, and that send is rejected instead. A template with this setting on cannot be used by a broadcast, because a broadcast sends one version to a whole audience and never names a language. |
You can set these two independently. On its own, fail only applies when a send names a language we do not have, so a send that names none still gets through. Turn on both settings together when you want every send to name a language on purpose.
Personalizing with variables
Write {{ variable }} placeholders in the subject and body. We pick them up automatically, combined across every language, so you never have to declare them separately. There is one way to tell the two kinds of placeholder apart: a path starting with bird. reads from our own data, either a contact record or the unsubscribe link, and everything else is a parameter you give a value for when you send.
A parameter's name is a single word, like {{ animal }}. A dotted name reaches for a structure a parameter does not have, so publishing one is rejected: write the value as its own parameter, or read contact data with bird.contact.<attribute> instead.
On a single send or a batch, a parameter's value comes from the send's template.parameters object, keyed by its name. One set of values covers every recipient of that send. bird is the one name you cannot use there: a template.parameters key named bird is rejected with a 422.
On a broadcast, there is no parameters object, so its content can only use bird. placeholders. bird.contact.<attribute> is filled in from each recipient's own contact properties, which is what personalizes the content per recipient. Every contact property is available by its own key, and so are the three built-in fields: first_name, last_name, and email.
代码示例
Hi {{ bird.contact.first_name }},Every parameter your content uses needs a value when you send, or the send is rejected with a 422 naming the parameter that is missing. Give a value for every parameter any of your languages use, not just the ones you expect to be picked, because you cannot always predict which language a given send ends up using. A contact property that a contact has no value for renders as nothing rather than failing the send, so give anything customer-visible a fallback: {{ bird.contact.first_name | default: "there" }}.
A broadcast is stricter about which names it accepts, because contact properties are all it has to fill placeholders with. Its bird.contact.* placeholders can only name a built-in field, or a contact property the workspace has registered and has not archived. Any other placeholder, including a parameter, is one the broadcast has no way to fill. Sending is refused, and the error names the placeholder. An archived property is refused exactly like one that was never registered at all.
Placeholders use Liquid, so filters and control flow work alongside plain substitution. A {% if %} conditional and a {% for %} loop over an array value are both fine. A handful of constructs are rejected when you publish, and the error names exactly what to change:
- Partial includes, using {% include %} or {% render %}.
- The increment, decrement, and ifchanged tags.
- The money, format_date, format_time, json, inspect, and type filters.
- Comparing against empty or blank. Use .size == 0 instead.
- Blocks nested far deeper than real email markup needs.
A broadcast's template cannot use a {% for %} loop at all, because a broadcast fills in one value per contact property and has nothing to iterate over. If your content needs a loop, send it through the messages API instead.
Values that get substituted into the HTML body are escaped for you, so a contact whose name contains & or < cannot break your markup or inject a script. The subject line and the plain-text body are left exactly as you wrote them. When the body uses Liquid tags or filters, publishing writes that escaping into the stored markup, so the HTML you read back from such a version is not byte-identical to what you submitted.
Previewing before you publish
Render a template with sample values and get back the subject line and the HTML and plain-text bodies a send would deliver. Preview renders the draft by default, which is how you check a change before it goes live, and it can render a published version instead. It works for your own templates and for our built-in ones, and nothing is actually sent.
If the draft has personalization that would be rejected when you publish, preview returns that same error, so it doubles as a way to find problems early.
Sending with a template
Set the send's template field to an object that names the template, either by id (emt_...) or by slug, using exactly one of the two. Put its variable values in template.parameters. Add language to pick a specific language, or leave it out to send the template's default language, unless the template requires every send to name one. Leave subject, html, and text out entirely, because the template already provides them.
代码示例
{
"from": "hello@yourdomain.com",
"to": ["delivered@messagebird.dev"],
"category": "transactional",
"template": {
"slug": "welcome-email",
"parameters": { "first_name": "Jane" }
}
}One behavior worth planning for: the template's category is a default, and the send's own category overrides it. Leave category off, and the send inherits the template's category, so an operational template sends as transactional without you repeating it on every call. Set category, and your value wins instead. The rest of the send-side contract is in sending with a template.
Authoring outside the dashboard
The whole lifecycle is available outside the dashboard. The publish step is named submit there, and it is the operation that turns the draft into the next published version:
代码示例
bird email templates create welcome-email --category marketing --source html
bird email templates versions languages set <emt_...> <emv_...> en --subject "Hi {{ bird.contact.first_name }}" --html "<p>Hello</p>" --yes
bird email templates versions submit <emt_...> <emv_...> --validate-only --yes # report problems, freeze nothing
bird email templates versions submit <emt_...> <emv_...> --yes # freeze, go livecreate returns the template along with its draft_version_id, which every version and language command takes. --validate-only runs the same completeness checks as a real submit, without freezing anything, so it is the cheap way to find every problem across every language in one pass. Reading a template back gives you its metadata and its per-language state, but no content. Content lives on a version's languages, one language at a time.
An agent reaches the same operations through the email_templates_* MCP tools.
Next steps
- Sending email: the full send payload, and how templated sends fit it
- Categories: choosing marketing vs transactional on each send
- bird email templates: managing templates from the terminal
- MCP server: letting an agent author and publish templates