WhatsApp templates
Business-initiated WhatsApp messages are sent from a template: a pre-approved message that WhatsApp has reviewed for its category. A template carries fixed text plus {{n}} variables, so a send only supplies the values that change (an OTP code, an order number) and WhatsApp assembles the final message. Today every WhatsApp send names a template; there is no free-text send yet.
Templates are Bird-managed today: a catalog of templates Bird has registered with WhatsApp, which you send from rather than authoring your own. The catalog is stocked per region, so the templates you see are the ones available for the region you call. The Templates page shows what's available and exactly how each one renders.

Browsing templates in the dashboard
The Templates tab, under WhatsApp, lists every template available to your workspace. Search by name and filter by status or category to find one.
Each row shows the fields you need to pick and send a template:
- Status: whether the template is sendable overall. Every template in Bird's catalog reads active; a workspace-authored template can also read draft, pending, rejected, or inactive. This is an aggregate across the template's languages, so it is not the same thing as WhatsApp's verdict on any one language (see Status and language status).
- Name: the template's display label (for example bird_otp). Catalogue templates are Bird-managed (scope: system), so nothing you control renames it. To send, use the template's slug instead.
- Language: the languages the template is registered in (e.g. en, pt-BR).
- Category: authentication, utility, or marketing. The category governs how WhatsApp treats the message, which sender number Bird uses, and, with the destination country, the price.
- WABA: the WhatsApp Business Account the template lives on. Bird's catalog templates share a single Bird-owned account, so they read Bird-managed.
Click a row to open the template's detail.
What's in a template
The detail view renders the template the way a recipient sees it: a WhatsApp-style message bubble with the body text, any {{n}} variables highlighted, and the template's buttons, so you can confirm the wording and layout before you send.
Alongside the preview is a ready-to-run cURL example: a complete POST /v1/whatsapp/messages call for that template, pointed at your region's host, with a components array pre-filled with the template's example values. Copy it, swap in your API key and real values, and send.
The example is the fastest way to see the shape a send has to match. Over the API, the same content comes from the template's version (Reading a template's content).
Listing templates from the API
GET /v1/whatsapp/templates returns the catalog, cursor-paginated. Reading templates needs an API key with the whatsapp_management scope. Reach it over HTTP, or from an SDK through that client's raw-request method.
type Templates = { data: Array<{ slug: string; status: string }> };
const templates = await bird.request<Templates>({
method: "GET",
path: "/v1/whatsapp/templates",
});templates = client.get("/v1/whatsapp/templates")var out struct {
Data []struct {
Slug string `json:"slug"`
Status string `json:"status"`
} `json:"data"`
}
if err := client.Get(context.Background(), "/v1/whatsapp/templates", &out); err != nil {
log.Fatal(err)
}$templates = $bird->get('/v1/whatsapp/templates');curl https://us1.platform.bird.com/v1/whatsapp/templates \
-H "Authorization: Bearer $BIRD_API_KEY"Each entry identifies a template and says where it stands. It does not carry the message text: content lives under a version, which you read separately (Reading a template's content).
Code example
{
"available_languages": ["en", "es", "pt-BR", "..."],
"category": "authentication",
"default_language": "en",
"description": "One-time passcode",
"id": "wat_01ky4x8e4genzb7way45txfkm1",
"languages": {
"en": { "status": "approved" },
"es": { "status": "approved" },
"pt-BR": { "status": "approved" },
"...": "..."
},
"name": "bird_otp",
"on_missing_language": "fail",
"scope": "system",
"slug": "bird_otp",
"status": "active"
}bird_otp is registered in 70 languages; the excerpt above shows a few. Every one of them appears in a real response.
The fields a send depends on:
- slug: the handle you send by. Handles beginning with bird_ are Bird's built-in templates.
- available_languages: the languages a send can resolve right now. A language leaves this set whenever WhatsApp pauses or limits it, without anyone having edited the template.
- on_missing_language: what a send does when the language it asks for has no approved copy. fail rejects the send; fallback sends default_language instead.
Status and language status
Two vocabularies sit in this response, and the example above shows both. status is the template's lifecycle (draft, pending, active, rejected, inactive), aggregated over its languages; every template in Bird's catalog is active. languages.<tag>.status is WhatsApp's verdict on one language, approved among them.
They can disagree: a template reading active can still hold a rejected or paused language. Read available_languages for the short answer about what you can actually send.
Reading a template's content
The text and its placeholders live on one language of one version, so reading them is two hops from the template. Take live_version_id (the version WhatsApp is serving) from the template, then ask for the language you intend to send:
const language = await bird.request({
method: "GET",
path: "/v1/whatsapp/templates/bird_order_confirmation/versions/{version_id}/languages/en",
});language = client.get(
"/v1/whatsapp/templates/bird_order_confirmation/versions/{version_id}/languages/en"
)var language map[string]any
if err := client.Get(context.Background(),
"/v1/whatsapp/templates/bird_order_confirmation/versions/{version_id}/languages/en",
&language); err != nil {
log.Fatal(err)
}$language = $bird->get('/v1/whatsapp/templates/bird_order_confirmation/versions/{version_id}/languages/en');curl https://us1.platform.bird.com/v1/whatsapp/templates/bird_order_confirmation/versions/{version_id}/languages/en \
-H "Authorization: Bearer $BIRD_API_KEY"The template reference accepts the slug or the wat_ id. GET …/versions/{version_id}/languages lists every language on the version if you want them all in one call.
Code example
{
"category": "utility",
"components": [
{
"example_parameters": [
{ "name": "ref", "text": "A1B2C3D4", "type": "text" },
{ "name": "amount", "text": "EUR 49.99", "type": "text" }
],
"text": "Your order {{ref}} has been confirmed for a total of {{amount}}. Thanks for shopping with us.",
"type": "body"
}
],
"language": "en",
"status": "approved"
}components is what a send has to match, and example_parameters names each placeholder. Here a send passes name: "ref" and name: "amount" on its two body parameters. Where the parameters carry no name, the template is positional: bird_otp renders *{{1}}* is your verification code, so its values go in {{n}} order and must not carry a name. A template with buttons carries them as their own component, and a button that takes a value has its own example_parameters your send must fill.
Note the category here is Meta's category for this language, which is what the message is priced at. The template's own category is the one it was registered under, and Meta can move a language out of it.
The version itself also reports a variables list, the same shape email and SMS templates use: one entry per placeholder with its key, type, whether it is required, and a human-readable constraint. A template with named placeholders keys them by name; a positional one, like bird_otp, keys them by their {{n}} position, as 1, 2 and so on. Read it when you want the whole contract in one place rather than walking the component tree.
Sending with a template
Name the template in the send's template object and fill its variables through components; see Sending WhatsApp messages for the full payload:
const msg = await bird.whatsapp.send({
to: "+15551234567",
template: {
slug: "bird_otp",
components: [{ type: "body", parameters: [{ type: "text", text: "123456" }] }],
},
});
console.log(msg.id, msg.status);msg = client.whatsapp.send(
to="+31612345678",
template="bird_otp",
language="en",
components=[{"type": "body", "parameters": [{"type": "text", "text": "123456"}]}],
)
print(msg.id, msg.status)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
msg, err := client.Whatsapp.Send(context.Background(), bird.WhatsappSendParams{
To: "+15551234567",
Template: "bird_otp",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)
}$message = $bird->whatsapp->send(
to: '+15551234567',
template: 'bird_otp',
language: 'en',
);
echo $message->getId(), ' ', $message->getStatus();bird whatsapp send \
--components '[{"parameters":[{"text":"1234","type":"text"}],"type":"body"},{"parameters":[{"text":"1234","type":"text"}],"type":"button"}]' \
--language en \
--template bird_otp \
--to +31612345678curl -X POST https://us1.platform.bird.com/v1/whatsapp/messages \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155550100",
"template": {
"slug": "bird_otp",
"language": "en",
"components": [
{ "type": "body", "parameters": [{ "type": "text", "text": "481920" }] },
{ "type": "button", "parameters": [{ "type": "text", "text": "481920" }] }
]
}
}'Next steps
- Sending WhatsApp messages: the full send payload the template object slots into
- WhatsApp log: find a sent message and follow its lifecycle
- WhatsApp pricing: how category and destination set the price