Documentation
Sign inGet started

Authoring WhatsApp templates

Bird's managed catalog covers the common cases, but a template that speaks in your own words has to be authored on a WhatsApp Business Account you have connected. This page covers creating one; WhatsApp templates covers browsing and sending what already exists.
Three things shape the whole flow:
  • A template holds versions, and a version holds one entry per language. What actually sends is a language of a version, not the template.
  • Content is written to a draft. A template has at most one open draft, and nothing in it reaches WhatsApp until you submit it.
  • Approval comes back per language. One language can be approved while another on the same version is rejected.

Before you start

You need a number of your own connected, which is what gives your workspace a WhatsApp Business Account to author on. An account you have not connected is refused, and so is editing one of Bird's built-in bird_ templates: those live on Bird's own account, so duplicate one onto yours instead.
Authoring an authentication template additionally needs a verified business; utility and marketing do not. See Authentication templates for that gate.
Author a template in the dashboard under WhatsApp > Templates, with the bird CLI, or through the MCP server. The dashboard walks the same steps this page describes; the examples further down use the CLI.

In the dashboard

New template offers two ways in. Start with a template opens the gallery, the fastest route: pick one that already says nearly what you need, including one of Bird's, and the copy lands on your account as an open draft.
The template gallery in the Bird dashboard: a grid of template cards, each previewing its message and labelled with its name, slug, status, category, and languages, beside filters for template source, category, and language
Start from scratch asks for the category, a name, and a default language before opening the editor. A marketing template also picks a message type. The name becomes the slug, and the slug and the category are the two choices you cannot change later.
The Create a new template step in the Bird dashboard: Marketing, Utility, and Authentication category tiles above a Name field and a Default language selector, with a Create template button
The editor writes one language at a time: the sidebar lists the template's languages with each one's review state, the middle column holds the content, and the phone preview renders the message with example values substituted.
The template editor in the Bird dashboard for the Order update utility template: English marked Approved beside Dutch in the language sidebar, and a phone preview of the rendered message carrying its Track order and Contact support buttons
The editor changes shape with the template. A carousel adds a tab per card beside the message, and every card has to repeat card 1's structure: the same header format and the same buttons in the same order.
The template editor in the Bird dashboard for a carousel marketing template: Message, Card 1, Card 2, and Card 3 tabs above the message body, with a Variable samples section below, beside a phone preview showing the message followed by swipeable image cards each with a Show me button
An authentication template has no message editor. WhatsApp writes the copy, so the editor offers only the two settings it writes from: Add security recommendation and Code expiration (minutes).
The template editor in the Bird dashboard for an authentication template: an Authentication settings panel with an Add security recommendation toggle and a Code expiration (minutes) field, beside a phone preview of the verification-code message WhatsApp writes, with its Copy code button
Save as draft keeps your work without contacting WhatsApp. Submit for review freezes the version and sends it to WhatsApp. The CLI's submit, below, performs the same freeze.

Two ways to start

Duplicate an existing template

A duplicate carries the source's content as an open draft and calls WhatsApp zero times, so nothing is submitted until you choose to. Two things about a copy are worth knowing before you make one:
  • The category is inherited and cannot be changed. If you need a different category, start from scratch instead.
  • You can narrow the languages, never add to them. A catalog template stocking 70 languages does not have to become 70 languages of yours: pick the subset you will actually maintain. Asking for a language the source does not carry is refused with E15060, and the response names which ones did not match. Add further languages to the copy afterwards.
The language subset is an array, so it goes in a request body rather than a flag:
Esempio di codice
bird whatsapp templates duplicate bird_order_confirmation --body-file copy.json
Esempio di codice
{
  "waba": "102290129340398",
  "slug": "acme_order_update",
  "include_languages": ["en", "es-ES"],
  "default_language": "en"
}
Omit include_languages and the copy takes all of the source's languages. Omit default_language and the copy keeps the source's default when your subset still carries it; otherwise it takes the first of the copy's languages by canonical tag, which is not necessarily the first one you listed, so set it explicitly if it matters.

Start from scratch

Creating a template takes a slug, an account, a category, and a default language:
Esempio di codice
bird whatsapp templates create order_update \
  --waba 102290129340398 \
  --category utility \
  --default-language en
The slug and the category are both permanent. WhatsApp derives its own template name from the slug, and neither it nor the category can be changed later; a different one means a new template. The bird_ prefix is reserved for Bird's catalog. The category you choose is not necessarily what a send is priced at: Meta applies its own category per language and can move one, and the price follows Meta's.

Writing each language

Open the draft, then write one language at a time:
Esempio di codice
bird whatsapp templates versions create order_update
bird whatsapp templates versions languages set order_update <version-id> en --body-file en.json
Opening a draft is safe to repeat: a template has only one, so this returns the open one rather than creating a second. The template also reports it as draft_version_id.
A language write replaces the language, it does not merge into it. The file carries that language's complete components every time, so read the language first and write it back whole; sending only the block you changed deletes the rest.
Every variable needs an example value. WhatsApp reviews the filled-in message rather than the template, so a block with placeholders and no example parameters is refused at submit rather than at write time.

Check, then submit

Validate before you freeze anything. A validate-only submit runs every check across every language and reports each problem in one pass, sending nothing to WhatsApp:
Esempio di codice
bird whatsapp templates versions submit order_update <version-id> --validate-only
Read valid and errors; each error names the language, the field, and the code a real submit would fail with. Then submit for real by dropping the flag. That freezes the draft as an immutable version and answers 202. Use a different idempotency key for the check and the submit, since reusing one key with a changed body is rejected.
Only languages whose content differs from their approved copy go to WhatsApp. One that already matches carries its approval forward, so a submit where nothing changed settles immediately with nothing to poll. No replacement draft opens afterwards: the next round of edits starts by creating a draft again.
A clean validate-only run does not predict WhatsApp's decision. WhatsApp offers no way to ask in advance, so it can still refuse content that passed every local check.

Watching the review

Approval arrives later and per language. The template's pending_version_id stays set while any language is unresolved, and the per-language list carries each verdict:
  • approved sends. available_languages on the template is exactly what a send can resolve right now.
  • rejected, submit_failed, paused need an edit on a fresh draft. WhatsApp accepts an edit to a paused language, and resubmitting is what clears it.
  • disabled, limit_exceeded, in_appeal refuse an edit outright; they only need re-reading until WhatsApp moves them.
The template's own status is an aggregate: active means at least one language is sendable, not all of them.

Sending what you authored

An authored template sends through the same endpoint as any other, with one difference from Bird's catalog: you must name from, and it has to be a number on the same WhatsApp Business Account as the template. A sender on a different account is refused 422 E15023 before anything is charged.
Naming a language is optional and resolves to the template's default, unless the template requires one explicitly. The values you supply have to fill the placeholders of whichever language actually resolves, so read that language's content before sending. See How sending works for the full payload.

Things to watch

  • The newest version is not the one that sends. A version list is newest-first and includes any open draft, so the top row is often a draft or a version still under review. The template names the version in service as live_version_id; a template with no live version cannot be sent at all.
  • A language under review refuses a write. WhatsApp holds it until the review finishes, so an edit during pending fails rather than queueing.
  • A list row carries no content. Listing templates finds them and shows lifecycle state; reading what one actually says takes a version read.
  • Deleting is unrecoverable. Discarding a language, deleting a draft, and deleting a template all take an explicit confirmation, and deleting a template stops every send by that slug.

Next steps