---
title: "Create a message template"
canonical: "https://bird.com/docs/api/reference/create-whatsapp-template"
---

# Create a message template

`POST /v1/whatsapp/templates`

Creates a WhatsApp message template. Supply `languages` to seed its first draft in the same call, or omit it to create the template alone and add content through the version and language paths. Nothing here reaches Meta: content is submitted for review as a separate, explicit step, so a template this call returns is `draft` and not yet sendable.

## Servers

- `https://{region}.platform.bird.com`: Regional API endpoint. Use the host for the region your organization is hosted in. Official Bird SDKs and the CLI select it automatically from your API key, so you rarely need to set it by hand.
  - `region`: `us1`, `eu1`. Default: `us1`. The region your organization's data is hosted in.
- `https://platform.bird.com`: Region-independent endpoint for authentication and account administration.
- `http://localhost:8080`: Local development.

## Authentication

Use one authentication option:

- `BearerAuth`: HTTP bearer authentication. Pass the API key as a bearer token in the `Authorization` header. Workspace
keys use `bk_{region}_*`; organization master keys use `bm_{region}_*`.
The prefix identifies the region and selects the
API endpoint. Official Bird SDKs and the CLI derive the region from the key.

- `CookieAuth`: Send `bird_session` in cookie. Session cookie set after signing in to the Bird dashboard. The cookie
value is an opaque session token; no session data is stored in the cookie
itself.

## Code samples

**TypeScript**

```ts
const template = await bird.whatsapp.templates.create({
  waba: "102290129340398",
  slug: "order_update",
  name: "Order update",
  category: "utility",
  default_language: "en",
  languages: {
    en: {
      components: [
        {
          type: "body",
          text: "Your order {{order_id}} has shipped.",
          example_parameters: [{ type: "text", name: "order_id", text: "A-4192" }],
        },
      ],
    },
  },
});
console.log(template.id, template.draft_version_id);
```

Examples: [TypeScript](/docs/api/reference/create-whatsapp-template.ts.md) · [Python](/docs/api/reference/create-whatsapp-template.py.md) · [Go](/docs/api/reference/create-whatsapp-template.go.md) · [PHP](/docs/api/reference/create-whatsapp-template.php.md) · [CLI](/docs/api/reference/create-whatsapp-template.cli.md) · [MCP](/docs/api/reference/create-whatsapp-template.mcp.md) · [cURL](/docs/api/reference/create-whatsapp-template.curl.md)

## Example response `201`

```json
{
  "id": "wat_01krdgeqcxet5s7t44vh8rt9mg",
  "slug": "bird_otp",
  "name": "Order update",
  "description": "Sent when an order ships.",
  "scope": "system",
  "waba": "102290129340398",
  "category": "authentication",
  "status": "active",
  "default_language": "pt-BR",
  "on_missing_language": "fallback",
  "language_source_required": false,
  "available_languages": [
    "pt-BR"
  ],
  "languages": {
    "en": {
      "status": "approved"
    }
  },
  "draft_version_id": "wav_01krdgeqcxet5s7t44vh8rt9mg",
  "live_version_id": "wav_01krdgeqcxet5s7t44vh8rt9mg",
  "pending_version_id": "wav_01krdgeqcxet5s7t44vh8rt9mg",
  "last_submitted_at": "2026-07-26T16:40:00Z",
  "next": [
    {
      "kind": "operation"
    }
  ]
}
```

## Header parameters

- `Idempotency-Key` (string, optional)

  Client-supplied key. On operations supporting request deduplication, a retained
  response is replayed for duplicate requests with the same key within the
  idempotency window (3 hours by default). This protection requires a workspace,
  organization, or staff-account scope. User-only and unscoped unauthenticated operations,
  streams, and operations with a separate replay contract do not use this
  response replay.

  On a supported operation, if idempotency protection is unavailable before execution, the API returns
  `503 IdempotencyUnavailable` (E01033) without executing this attempt. Retry with
  backoff using the same key and request. An operation that takes effect before
  its response is retained can still execute again on retry.

  Two distinct 409 errors signal misuse:

  - `request_in_progress` (E01004): The same key is currently being
    processed by a concurrent request. Wait briefly and retry. The lock expires within 30 seconds.
  - `idempotency_key_reuse` (E01005): The same key has already completed
    against a different request body or method. Generate a new key.

  Recommended key format is `<event-type>/<entity-id>` (for example `welcome-user/usr_abc123`).
- `X-Workspace-Id` (string, optional): Workspace context for the request. Required for dashboard authentication and master keys. Workspace keys and access tokens already identify their workspace; send that workspace or omit the header. A different one is rejected.
- `X-Organization-Id` (string, optional): Organization context for the request. Required for dashboard authentication. An API key or access token carries its own organization, so send either that organization or no header at all; a different one is rejected.

## Request body

- `waba` (string, required): The WhatsApp Business Account to author this template on. A template belongs to one account at Meta and is reviewed, approved and sent under it, so every create names the account explicitly. It must be an account connected to this workspace.
- `slug` (string, required): The template's workspace-unique handle, and what a send references. Editable only before the first submission. The `bird_` prefix is reserved for our built-in templates and is rejected. Meta's own template name is derived from this handle, so the handle itself is validated against the charset Meta accepts.
- `name` (string): The template's display name, shown wherever the template is listed. Free text, changeable at any time. Defaults to the slug when omitted.
- `description` (string): What the template is for. Free text.
- `category` (string, required)

  The category Meta reviews the template under. Required, because Meta demands one on every template it holds and it determines what a send costs. There is no default. Meta can recategorize a template after review; the per-language summary reports what it actually holds.

  Possible values: `authentication`, `utility`, `marketing`
- `default_language` (string, required): The language a send uses when it names none, and the last resort when a requested language is unavailable. When `languages` is supplied it must be one of its keys.
- `on_missing_language` (string)

  How a send behaves when it asks for a language this template does not carry. Defaults to `fail`: a WhatsApp send that silently fell back would deliver approved copy the recipient did not expect, at a price the sender did not choose.

  Possible values: `fallback`, `fail`
- `language_source_required` (boolean): Whether a send has to name a language. Set it to have a send that names none rejected, leaving the default language for sends that ask for it.
- `languages` (object)

  Optional seed for the template's first draft, keyed by BCP-47 language
  tag. Supplying it opens the draft and writes every language in one
  transaction: any invalid entry fails the whole create, leaving no
  template behind. Tags are canonicalised, so `pt-br` lands as `pt-BR`, and
  two keys canonicalising to one tag are rejected.

  Omit it to create the template with no draft, and add content later
  through the version and language paths. Nothing here reaches Meta:
  content is submitted, never created.

## Response body

- `id` (string, required): Stable Bird identifier for the template.
- `slug` (string, required): The template's handle, editable before the first submission. Address it by this handle, and reference it when sending. Handles beginning with `bird_` are reserved for our built-in templates.
- `slug_editable` (boolean, required): Whether the slug can still be changed. False after the first submission and for built-in templates.
- `name` (string, required): A display name for the template. Nothing resolves through it, so it is safe to show wherever a human reads the template.
- `description` (nullable string, required): What the template is for. Null when unset.
- `scope` (string, required)

  Whether the template is one of our built-in templates (`system`) or one your workspace created (`workspace`).

  Possible values: `system`, `workspace`
- `waba` (string): The WhatsApp Business Account that holds this template's languages at Meta. Absent on a built-in template: those live on a WABA that Bird manages centrally rather than on your account, so it is not yours to reconcile against and is not disclosed.
- `category` (string, required)

  The category you declared for the template. It is fixed once the template exists. Meta applies its own category per language and may move one, which is what messages are priced at. Read the language for that.

  Possible values (may grow over time): `authentication`, `utility`, `marketing`
- `status` (string, required)

  The template's lifecycle, aggregated over its languages.

  Possible values: `draft`, `pending`, `active`, `rejected`, `inactive`
- `default_language` (string, required): The language a send is served in when it names none, whichever `on_missing_language` is set. A template that sets `language_source_required` refuses such a send instead. Under `fallback` it is also the last hop for a language that is not in `available_languages`, whether the template holds no copy in it or holds one WhatsApp has not approved. The template is required to hold this default, and it must itself be in `available_languages` for a send to resolve here.
- `on_missing_language` (string, required)

  What a send does when the language it asks for has no approved copy. Defaults to `fail` on WhatsApp, because every language is separately approved and separately priced: falling back silently would send content the recipient did not expect at a rate the sender did not choose.

  Possible values: `fallback`, `fail`
- `language_source_required` (boolean, required): When true, a send must name a language explicitly rather than letting the template resolve one.
- `available_languages` (array of string, required): The languages a send can resolve right now: approved and not held back by Meta. It shrinks for reasons you did not cause: Meta pauses, disables, archives or limits a language and it leaves the set with nobody having edited anything. Read `languages` to see which languages exist and why one is missing.
- `languages` (object, required): Where each of the template's languages stands, keyed by BCP-47 language tag. This is the summary of the version currently in service, so a template reading `active` can still hold a rejected or paused language: the aggregate says something is sendable, and this says which. Content is not here; it lives under a version.
- `draft_version_id` (nullable string, required): The open draft, or null when nobody is editing. Non-null is the answer to whether this template has unsubmitted work: a draft exists only because someone opened one.
- `live_version_id` (nullable string, required): The version Meta is serving. A version goes live as a unit the moment any of its languages is approved, superseding the one before it. Null until a first approval.
- `pending_version_id` (nullable string, required): A submitted version still awaiting verdicts: what to poll. It stays set while any language is unresolved, including after a sibling's approval took the version live. Null when nothing is outstanding.
- `last_submitted_at` (nullable string, required): When this template was last submitted. Null for a pre-approved built-in template.
- `created_at` (nullable string, required): When the template was created. Null for a built-in template, which Bird ships rather than stores.
- `updated_at` (nullable string, required): When the template was last modified. Null for a built-in template, which Bird ships rather than stores.
- `next` (array of object)

  What to do next with this template, given the state it is in. Each entry names one
  action and says why it is worth taking, so you can act on this response without
  working out the order yourself. Present on reads that compute it: an empty list
  means there is nothing to do, and the field is absent entirely on responses that
  do not report next actions.

  A `draft` template routes to opening its draft, a `pending` one to the version
  under review, and a `rejected` or `inactive` one to a fresh draft. The template's
  `status` is the aggregate over its languages, so an entry may send you to the
  version to see where each language actually stands.
- `next.kind` (string, required)

  What you do about this step.

  - `operation`: call the operation named in `operation`, then
    read again.
  - `external`: act somewhere this API does not reach, then read
    again.
  - `wait`: nothing is asked of you, so read again later.
  - `terminal`: nothing you do resolves this, so stop retrying.

  Tolerate a value you do not recognize: show the `description` and
  offer no action.

  Possible values (may grow over time): `operation`, `external`, `wait`, `terminal`
- `next.description` (string, required): A short, human-readable label for the step, suitable for display.
- `next.operation` (string): The operationId to call. Present only when `kind` is `operation`. The operation's own schema says how to call it; this says only which one, and what to address it with.
- `next.params` (object): The parameters that address the operation, by name: `{"sender_id": "…"}` for an operation on `/v1/sms/senders/{sender_id}/requirements`. A parameter the operation takes in its query string is given the same way, so an operation addressed as `?subject_id=` carries `{"subject_id": "…"}`. Every parameter the call needs is here, whether its value came from the thing you were acting on or is fixed for this step, so you can make the call from this object alone. Present only when `kind` is `operation` and the operation names a subject. A request body, when the operation takes one, is described by the operation's own schema and never appears here.
- `next.url` (string): A URL to open. Present only when `kind` is `external`, and only when the step has one. An external step whose `description` says to go and do something with no URL to open is normal.

## Related resources

- [Should I use a Bird SDK or call the API directly?](/explained/platform/should-i-use-an-sdk-or-call-the-api-directly) (answer)
- [Build your first integration](/learn/paths/integration) (course)
- [Send your first email](/docs/get-started/send-your-first-email) (docs)

[Get an implementation brief](/learn/workspace?topic=api-basics)
