WhatsApp

What is a WhatsApp message template?

Outside an open conversation, a template is the only message that reaches someone on WhatsApp. Meta reviews it before you can send it, and the approved copy is what goes out with your variables filled in. That much every page about WhatsApp says, and it is where most of them stop.

What it leaves out is that "approved" is not a property of the template. It is a property of one language inside one version of it, and almost everything that surprises people later follows from that.

What is actually inside a template?

Three things, and only the first is the one people picture.

A stable identity. The template has an id, a display name that nothing resolves through, and a slug, which is the immutable handle you address it by and reference when sending. Two prefixes are reserved and refused on create: bird_, which belongs to Bird's built-in templates, and wat_, which is the template id prefix.

A declared category, one of authentication, marketing or utility. You choose it at creation and it is fixed from then on.

Content, per language, inside a version. This is the part that matters. The template itself holds no message content at all. Each language's copy is built from content blocks: a body block is required, and header, footer, buttons and carousel are optional and appear at most once each. Those blocks belong to a version, so reading a template's actual words is two steps rather than one: take the version you want from the template, then request the language from that version. Usually the version you want is live_version_id, the one Meta is serving, but it is null until a first approval, so a template still in draft or still under review has content and no live version. The three version pointers are below.

So a template is closer to a folder than to a message. The templates product page shows that shape in the dashboard, WhatsApp templates walks the read path, and authoring WhatsApp templates covers writing one.

Why might my messages be priced as a category I did not declare?

Because two parties set the category, and only one of them is you.

The category field on the template is the category you declared. It is fixed once the template exists. Meta applies its own category per language, and it can move one after the fact, which is what messages on that language are priced at. Bird records the change when Meta reports it, so the move is visible rather than silent.

The practical rule is that the template's category tells you what you asked for and the language tells you what you got. Anything that depends on the answer, including cost, has to read the language. Current rates are on the WhatsApp pricing page, which is the only place a live figure belongs.

This is also why a miscategorised template is worth catching at authoring time rather than after a month of sends: you cannot correct the declared category later by editing the template, because that field does not move.

Which of my languages can I actually send?

The ones in available_languages, and that set is narrower than "the languages I wrote".

A template carries two different status fields and they answer different questions. The template's own status is an aggregate over its languages: active means at least one language is live, so something can be sent. It does not mean the language you are about to name is one of them. A template reading active can hold a rejected, paused or archived language at the same time.

languages is the per-language answer, keyed by BCP-47 tag, and it says both where a language stands and why. A language can read approved, pending, rejected, paused, disabled, in_appeal, pending_deletion, limit_exceeded, archived, deleted, submit_failed or outcome_unknown, and the set is open, so accept a value you do not recognise rather than treating the response as invalid.

available_languages is the resolved answer: the languages a send can use right now. It shrinks for reasons you did not cause. Meta can pause, disable, archive or limit a language with nobody on your side having touched anything, and the language leaves the set. Treat it as live state to read before a send rather than a list to cache at deploy time.

One field follows from this and is worth setting deliberately rather than inheriting. on_missing_language decides what a send does when the language it asked for has no approved copy, and on WhatsApp it defaults to fail. The reasoning is that every language is separately approved and separately priced, so quietly substituting another one would send content the recipient did not expect at a rate the sender did not choose. You can set it to fallback if that trade suits you, at creation or later on the template itself, which is the contrast with the category worth holding on to: the category is fixed for good, this is not. Updating a template leaves the current setting alone unless you send the field, and the fail default belongs to a create rather than being reapplied by a partial update. Only Bird's built-in templates have the policy fixed rather than settable.

fail is also narrower than the name suggests. It still matches a broader form of the tag you asked for, so a request for pt-BR is served by a stocked pt. What it refuses is a sibling variant: pt-PT never stands in for pt-BR.

Why can't I just edit an approved template?

Because the version you submitted is immutable, by design. There is no editing in place on either side.

Changing anything means opening a fresh draft, writing the language again, and submitting it, which is a new review. The three version pointers on the template tell you where you are:

  • draft_version_id is non-null only because somebody opened a draft, so it is the direct answer to whether unsubmitted work exists.
  • pending_version_id is a submitted version still awaiting verdicts, and it is what to poll. It stays set while any language is unresolved.
  • live_version_id is the version Meta is serving. A version goes live as a unit the moment any one of its languages is approved, superseding the version before it.

That last one catches people: approval of a single language promotes the whole version, so a version can be live while some of its languages are still pending or refused.

Meta also limits how often an approved language may be edited. Each language carries an editable_at timestamp giving the next moment an edit is allowed when that limit is currently spent, and a null there is permission rather than a guarantee, because a second limit over a longer window applies as well. Meta publishes those edit limits on template management and its review time on template fundamentals.

How do I send one?

Name the template by its handle and supply the values its variables expect. On the command line:

bird whatsapp templates list

That gives you the handles, the aggregate status and the languages. bird whatsapp templates get returns one template, and bird whatsapp templates versions languages get returns the actual content of one language in one version, which is the shape a send has to match. Every one of these is on the MCP server too, so an agent holding your credentials can read a template before it sends against it. Template management is deliberately a command-line and MCP surface rather than an SDK one, so reach for those rather than looking for a typed SDK method.

The dashboard's template detail also renders a ready-made cURL example for POST /v1/whatsapp/messages using that template's own example values, which is the fastest way to see the payload shape without assembling it by hand.

Sending WhatsApp messages covers filling variables and what the 202 does and does not promise, and the templates command reference carries the flags in full.

Build on the same network.

A test API key is yours immediately. Production unlocks when you add a payment method and verify a sender.

एक चैनल से शुरुआत करें।
तैयार होने पर बाकी जोड़ें।

एक test API key तुरंत आपकी है। जब आप payment method जोड़ते हैं और sender verify करते हैं, तब production अनलॉक हो जाता है।

Claude Code, Cursor या Codex इस्तेमाल कर रहे हैं? एक setup prompt कॉपी करें और आपका agent आपके लिए Bird CLI और skills इंस्टॉल कर देगा। अपना चुनें:

Cursor