# Sending WhatsApp messages

This guide covers the send endpoint, `POST /v1/whatsapp/messages`. You build one JSON payload with a recipient and a template; Bird returns `202 Accepted` with a message ID and delivers asynchronously. Business-initiated WhatsApp is template-only: you send a pre-approved template, not free text. Each request sends one message to one recipient, and there is no batch endpoint.

## A minimal send

The smallest valid payload is a `to` recipient and a `template` with its `name`. Include `language` unless the template has a single language, and fill any `{{n}}` variables the template declares through `components`.

### curl

The snippet calls the US host; if your key starts with `bk_eu1_`, call `https://eu1.platform.bird.com` instead.

```bash
curl -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": {
      "name": "bird_order_confirmation",
      "language": "en",
      "components": [
        { "type": "body", "parameters": [
          { "type": "text", "text": "A1B2C3D4" },
          { "type": "text", "text": "EUR 49.99" }
        ] }
      ]
    }
  }'
```

### TypeScript

```bash
npm install @messagebird/sdk
```

The SDK reads the region from your key, so you don't set a host:

```typescript
import { BirdClient } from "@messagebird/sdk";

const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });

const msg = await bird.whatsapp.send({
  to: "+14155550100",
  template: {
    name: "bird_order_confirmation",
    language: "en",
    components: [
      {
        type: "body",
        parameters: [
          { type: "text", text: "A1B2C3D4" },
          { type: "text", text: "EUR 49.99" },
        ],
      },
    ],
  },
});

console.log(msg.id, msg.status); // "wam_…", "accepted"
```

The `202` response is the accepted message: it echoes the resolved template with the values you filled in, and `from` names the Bird-managed number the message goes out on. There is no `from` field on the request; Bird selects the sender from the template's category.

```json
{
  "created_at": "2026-07-23T14:58:43.828Z",
  "delivered_at": null,
  "direction": "outbound",
  "from": { "phone_number": "+13124495550" },
  "id": "wam_01ky7qrtzme18862zq9bf0sby5",
  "read_at": null,
  "sent_at": null,
  "status": "accepted",
  "template": {
    "category": "utility",
    "components": [
      {
        "parameters": [
          { "text": "A1B2C3D4", "type": "text" },
          { "text": "EUR 49.99", "type": "text" }
        ],
        "type": "body"
      }
    ],
    "language": "en",
    "name": "bird_order_confirmation"
  },
  "to": { "phone_number": "+14155550100" }
}
```

## Building up the payload

### Recipient

`to` is a single recipient in [E.164](https://en.wikipedia.org/wiki/E.164) format: a leading `+`, country code, and subscriber number, e.g. `+14155550100`. Bird validates the number, so a value that cannot be a real, dialable number (wrong length, unassigned prefix) is rejected with a `422` `WhatsAppInvalidRecipient` before anything is charged. One message goes to one recipient; there is no recipient array and no batch send, so reach many people with one call per recipient.

### Template

`template` names the pre-approved template to send:

- **`name`** (required): the template's name, e.g. `bird_order_confirmation`. It must match a template in your catalog (lowercase letters, digits, and underscores).
- **`language`**: the template's language tag, e.g. `en` or `pt_BR`. Omit it only when the template exists in a single language; when it is stocked in several, omitting `language` returns a `422` that names the available codes. The accepted message echoes the resolved language.
- **`components`**: the values that fill the template's `{{n}}` variables (see [Components and parameters](#components-and-parameters)). Omit it for a template that has no variables.

Browse your templates, their languages, and a rendered preview of each on the [Templates](https://bird.com/dashboard/w/whatsapp/templates) page.

### Components and parameters

Templates carry numbered variables: `{{1}}`, `{{2}}`, and so on. You supply their values through `components`. Each component names a `type` (`body` or `button`) and a `parameters` array; each parameter is `{ "type": "text", "text": "..." }`, applied in `{{n}}` order (the first parameter fills `{{1}}`). Only `text` parameters are supported today, and parameter counts must match the template's declared placeholders exactly, or the send returns a `422` `WhatsAppTemplateParameterMismatch`. A `header` component type also exists on the wire, but Bird manages header values itself, so a `header` entry supplied on a send is ignored.

For example, a one-time-passcode template whose body reads `{{1}} is your verification code` and whose button copies the code takes the code as both a body parameter and a button parameter:

```json
{
  "components": [
    { "type": "body", "parameters": [{ "type": "text", "text": "481920" }] },
    { "type": "button", "parameters": [{ "type": "text", "text": "481920" }] }
  ]
}
```

### Category and sender

Category and sender are properties of the template, not of the send. A template's category (`authentication`, `utility`, or `marketing`) determines how WhatsApp treats the message, which sender number Bird uses, and, together with the destination country, what the message costs. There is no `from` field on the request.

### Tags and metadata

Two optional fields attach your own context to a message; both come back on API reads and ride on every [webhook event](/docs/guides/whatsapp/events#webhooks) for the message:

- **`tags`**: up to 20 structured `{ "name": ..., "value": ... }` labels for low-cardinality dimensions you filter and report by (a campaign, an experiment variant). Names and values take ASCII letters, digits, underscore, and hyphen; names are capped at 32 characters and unique within a send, values at 64. Filter the message list by tag (`?tag=campaign` or `?tag=campaign:launch-week`), and the [Metrics](/docs/guides/whatsapp/tracking-and-metrics) page breaks delivery down by tag.
- **`metadata`**: one arbitrary JSON object, up to 2 KB serialized, for per-send context you don't need as a filter dimension (an internal order ID, a session reference).

```json
{
  "tags": [{ "name": "campaign", "value": "order-confirmations" }],
  "metadata": { "order_id": "ord_8271" }
}
```

### Field reference

| Field                 | Type           | Required | Limits / notes                                                                 |
| :-------------------- | :------------- | :------- | :----------------------------------------------------------------------------- |
| `to`                  | string (E.164) | yes      | One recipient per message                                                      |
| `template.name`       | string         | yes      | A template name from your catalog; lowercase letters, digits, underscores      |
| `template.language`   | string         | no\*     | Template language tag (e.g. `en`, `pt_BR`); omit for single-language templates |
| `template.components` | array          | no       | Fills `{{n}}` variables; component `type` is `body` or `button`                |
| `tags`                | array          | no       | Up to 20 `{name, value}` labels; name ≤ 32 chars, value ≤ 64, names unique     |
| `metadata`            | object         | no       | Arbitrary JSON, up to 2 KB serialized                                          |

\* `language` is optional only when the template has a single language.

## What isn't supported yet

The following are rejected today:

- **Free-form (non-template) sends**: WhatsApp sends are template-only in this release; a request without `template` fails with a `422`.
- **Non-text parameters**: media, location, and other parameter types are reserved; only `text` is supported.
- **Batch sending**: there is no `/v1/whatsapp/batches`; send one message per call.

## The async model: what 202 means

A successful send returns `202 Accepted` with a message ID and `status: accepted`. The `202` is returned only after the send is durably accepted; it is never accepted and then silently dropped. Hard failures you can fix (an invalid recipient, an unknown template name or language, a parameter mismatch) fail immediately with a `422`, and an organization whose wallet holds no balance at all is rejected up front. Actual delivery happens asynchronously: the message moves to `sent` when Bird hands it to WhatsApp, then to a terminal status (`delivered` or `failed`) when the receipt arrives, reported through [events](/docs/guides/whatsapp/events), [webhooks](/docs/guides/webhooks), and the read endpoints. A read receipt is surfaced separately as a `read_at` timestamp and a `whatsapp.read` event, not as a status.

One privacy note: for `authentication`-category templates the API never returns the filled-in values. The `202` echo and every later read carry an empty `components` array for those messages, so a passcode never resurfaces.

## Retrying safely

Send the `Idempotency-Key` header with a unique value per logical send, and retries become safe: if your first request succeeded but you never saw the response (timeout, dropped connection), replaying the same request with the same key returns the original result instead of sending, and charging for, a duplicate message. The replayed response carries an `Idempotency-Replay` header. See [idempotency](/docs/guides/idempotency) for key format and retention.

## Cost and billing

WhatsApp is priced per message, keyed on the template's category and the recipient's country; see [WhatsApp pricing](https://bird.com/pricing/whatsapp). Bird charges your wallet when it processes the accepted send, before the message is dispatched to WhatsApp, and the charge stands whether or not the message is later delivered. When the charge cannot go through after the `202` (the wallet cannot cover the send, or the route has no configured price), the message ends `failed` with the error code `insufficient_balance` or `price_not_found`, and nothing is charged.

## Next steps

- [WhatsApp overview](/docs/guides/whatsapp/overview): the channel, the dashboard app, and where everything lives
- [WhatsApp events](/docs/guides/whatsapp/events): follow delivery per message, over the API or webhooks
- [Templates](/docs/guides/whatsapp/templates): browse the catalog and read a template's variables
- [Idempotency](/docs/guides/idempotency): safe retries with the `Idempotency-Key` header