---
title: "Sending WhatsApp messages"
description: "Send a WhatsApp template or service message with POST /v1/whatsapp/messages: the request envelope, the customer service window, the async 202 model, and cost."
canonical: "https://bird.com/docs/guides/whatsapp/sending-whatsapp"
---

# Sending WhatsApp messages

This guide covers the send endpoint, `POST /v1/whatsapp/messages`. You build one JSON payload with a recipient and exactly one type of content: a pre-approved **template**, or a **service message** carrying text, an image, video, audio, a sticker, a document, a location, contact cards or something to tap. Bird returns `202 Accepted` with a message ID and delivers asynchronously. Which of the two you can send depends on the [customer service window](#the-customer-service-window). 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 `slug`. Add `language` if you want a specific one; omitting it sends the template's default language, and fill any variables the template declares through `components`.

The curl call names the US host; if your key starts with `bk_eu1_`, call `https://eu1.platform.bird.com` instead. The SDKs read the region from your key, so they set no host.

**TypeScript**

```typescript
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);
```

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

## The customer service window

Which of the two you can send depends on one piece of state: whether the **customer service window** is open.

The contact opens the window by messaging or calling your business number, and it stays open for 24 hours, resetting every time they message you again. While it's open you can send a **service message**, meaning any free-form content: text, image, video, audio, sticker, document, location, or interactive. Once it lapses, only a pre-approved **template** reaches them, and their reply to it reopens the window.

Bird tracks the window for you, so a service message sent into a closed one is refused before anything is created or charged: the request returns a `422` [`E15044`](/docs/api/errors/E15044) `WhatsAppServiceWindowClosed`. The check is best effort and fails open, so a `202` is not proof the window was actually open at dispatch; a window that lapses between accept and dispatch fails asynchronously, with `service_window_expired` on the message's `last_error`.

See [the customer service window](/docs/knowledge-base/whatsapp/customer-service-window) for the full lifecycle: what opens it, what resets it, and how it interacts with pricing.

## Building up the payload

### Recipient

`to` names one destination, given as a phone number, a business-scoped user ID, or a group ID. A phone number is in [E.164](https://en.wikipedia.org/wiki/E.164) format: a leading `+`, country code, and subscriber number, such as `+14155550100`. We validate 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. There is no recipient array and no batch send, so reaching many people who are not in one group means one call per recipient.

A [business-scoped user ID](/docs/guides/whatsapp/business-scoped-user-ids) such as `US.13491208655302741918` addresses a contact whose phone number you do not have, which is how you reply to a contact who reached you without one. Two things change: the sending number has to belong to the same business portfolio the ID is scoped to, and a one-time-passcode template needs a phone number. A Bird-managed one is refused at accept with a `422` `WhatsAppRecipientNotSupportedForTemplate`; an authentication template your workspace authored is accepted and then fails, since Meta requires a phone number for it.

`to` takes one more form: a WhatsApp group ID such as `wag_01krdgeqcxet5s7t44vh8rt9mg`, which sends to every participant of that group chat. A group send omits `from` and reports its delivery across the group rather than against one recipient, so [Sending to a WhatsApp group](/docs/guides/whatsapp/groups/sending) covers it on its own page.

### Template

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

- **`slug`** (required): the template's slug, such as `bird_order_confirmation`. It must match a template in your catalog (lowercase letters, digits, and underscores).
- **`language`**: the template's language tag, such as `en` or `pt-BR`. Omit it to send the template's default language; naming a language the template doesn't have returns a `422` that lists the ones it does. The accepted message echoes the resolved language.
- **`components`**: the values that fill the template's 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 variables, named (`{{ref}}`, `{{amount}}`) or numbered (`{{1}}`, `{{2}}`). You supply their values through `components`. Each component names a `type` (`body` or `button`) and a `parameters` array. Each parameter names its own `type` (`text`, `image`, `video`, `gif`, `document`, or `location`) and carries the matching field: `text` a plain string, `image`/`video`/`gif`/`document` a public `https` `url`, and `location` a point on the map. A template with named parameters requires a `name` on every parameter, matching exactly the names the template declares (see [Field reference](#field-reference)). A positional template omits `name` and takes its values in `{{n}}` order instead, so the first parameter fills `{{1}}`. Either way, parameters that don't match what the template declares return a `422` `WhatsAppTemplateParameterMismatch`. A `header` component type also exists on the wire: on a Bird-managed template it's dropped, since no Bird-managed template declares a header variable, but on a template your workspace authored it's forwarded, which is how a media-header utility or marketing template gets its image.

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, positionally (no `name`):

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

### Category and sender

A template's category (`authentication`, `utility`, or `marketing`) determines how WhatsApp treats the message and, with the destination country, what it costs.

Who owns the sender decides whether you name it:

- **A Bird-managed template** (its slug begins with `bird_`) sends from the number Bird keeps for that category, so omit `from`. Setting it returns a `422` `WhatsAppSenderNotAllowed`.
- **Anything else** names its own sender in `from`: a service message of any type, and any template your workspace authored. The number must be one your workspace owns. Omitting it returns a `422` `WhatsAppSenderRequired`, and a number the workspace cannot send from returns a `422` `WhatsAppSenderNotFound`. An authored template also has to sit on the same WhatsApp Business Account as the number, or the send returns a `422` `WhatsAppSenderWABAMismatch`.

[Phone number setup](/docs/guides/whatsapp/numbers/connect) covers both types of number and how a number of your own is connected.

### Service messages

Instead of `template`, carry exactly one of `text`, `image`, `video`, `audio`, `sticker`, `document`, `location`, `contact_cards`, or `interactive`. All nine are service messages, so they need an open [customer service window](#the-customer-service-window). Every one of them also requires `from`, a number your workspace owns; Bird's managed numbers cannot carry it.

- **`text`**: `{ "body": "..." }`, up to 4096 characters. Add `"preview_url": true` to render a link preview for the first URL in `body`.
- **`image`**, **`video`**, **`audio`**, **`sticker`**, **`document`**: each takes a public `https` URL WhatsApp fetches at send time (`url`), so a signed URL has to outlive the send. An `http` URL is rejected outright. WhatsApp fetches the file itself, so a URL it cannot reach, one serving an unsupported type, or a file above the size limit for its type is accepted and then fails, with `media_rejected` on the message's `last_error` and WhatsApp's own reason in `description`. `image`, `video`, and `document` also take an optional `caption`; `document` also takes an optional `filename`; `audio` takes an optional `voice` flag for a voice-note rendering.
- **`location`**: `{ "latitude": ..., "longitude": ... }` (both required, decimal degrees) plus optional `name` and `address`.
- **`contact_cards`**: an array of up to five contacts shared in one message. Each card's `name` needs `formatted_name` plus at least one other part (`first_name`, `last_name`, `middle_name`, `prefix`, or `suffix`); `phone_numbers`, `emails`, `urls`, and `addresses` each take up to ten entries, and `org` and `birthday` (as `YYYY-MM-DD`) are optional. A `phone_number` in E.164 earns that card a button that opens a chat with it.
- **`interactive`**: body text plus something to tap, in one of six types: reply buttons, a list menu, a link button, a media carousel, or a single button asking the recipient for their location or their phone number. [Interactive messages](/docs/guides/whatsapp/message-types/interactive) covers the wire shape of each type, the replies a tap produces, and the limits.

```json
{
  "to": "+16505551234",
  "from": "+13124495648",
  "text": { "body": "Your order shipped: https://example.com/track/A1B2C3", "preview_url": true }
}
```

A request carrying no content, or more than one type, is rejected with a `422`.

### Quoting a message

Set `in_reply_to_message_id` to a WhatsApp message ID to send your message as a reply to it, the way tapping reply in the WhatsApp app quotes a message. The recipient sees your message with the quoted one above it, and the field comes back on every read of the message.

It works the other way too: an inbound message that WhatsApp marks as a reply carries the quoted message's ID in the same field, which is how you tell which of your messages a reply answers. An inbound message WhatsApp does not mark carries no ID, and resolution can also miss. For reliable correlation, use explicit interactive reply identifiers with your application’s stored conversation or task state. Outgoing [`metadata`](#tags-and-metadata) stays on the outgoing record and is not automatically copied onto the reply.

The quote is resolved before the send is accepted, so a quote that cannot be rendered fails the request itself and nothing is created or charged. An id naming no message this workspace holds, or one older than the 15 days a message stays quotable, returns a `404` [`E15071`](/docs/api/errors/E15071) `WhatsAppReferencedMessageNotFound`. One naming a message that never reached WhatsApp, or a message from a different conversation than this send's `to` and `from`, returns a `422` [`E15072`](/docs/api/errors/E15072) `WhatsAppMessageNotQuotable`. If Bird cannot reach the store that answers the question, the send returns a `503` [`E15073`](/docs/api/errors/E15073) `WhatsAppMessageLookupUnavailable`, which is worth retrying. Quoting works on a template send and a free-form send alike.

```json
{
  "to": "+16505551234",
  "from": "+13124495648",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "text": { "body": "Yes, that slot is still free." }
}
```

### 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/webhooks#fields-every-message-event-carries) 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         | yes      | One recipient per message: an E.164 phone number, a [business-scoped user ID](/docs/guides/whatsapp/business-scoped-user-ids), which no one-time-passcode template accepts, or a [WhatsApp group ID](/docs/guides/whatsapp/groups/sending) (`wag_…`), which sends to every participant of that group |
| `from`                                    | string (E.164) | no\*\*   | Omit for a Bird-managed template, which picks its own sender, and for a group send, which uses the group's own number; required for a service message and for a template your workspace authored, and must be a number your workspace owns                                                           |
| `template.slug`                           | string         | no\*\*   | A template slug your workspace can send; Bird-managed slugs begin with `bird_`                                                                                                                                                                                                                       |
| `template.language`                       | string         | no\*     | Template language tag (`en`, `pt-BR`); omit to send the template's default language                                                                                                                                                                                                                  |
| `template.components`                     | array          | no       | Fills the template's variables; component `type` is `body` or `button`                                                                                                                                                                                                                               |
| `template.components[].parameters[].name` | string         | no†      | The placeholder this value fills, such as `ref`; required and must match the template's declared names for a named-parameter template, omitted for a positional one                                                                                                                                  |
| `interactive`                             | object         | no\*\*   | Body text plus one type of tappable content; a service message, so it needs an open service window. See [Interactive messages](/docs/guides/whatsapp/message-types/interactive)                                                                                                                      |
| `in_reply_to_message_id`                  | string         | no       | A WhatsApp message ID this workspace holds, quoted in the message you send; echoed on reads. See [Quoting a message](#quoting-a-message)                                                                                                                                                             |
| `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; omitting it sends the template's default language.
† `name` is required on every parameter for a named-parameter template. Omit it for a positional one. See [Components and parameters](#components-and-parameters).
\*\* Carry exactly one of `template` or a service-message content field (`text`, `image`, `video`, `audio`, `sticker`, `document`, `location`, `interactive`); see [Service messages](#service-messages).

## 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 fail immediately with a `422`: an invalid recipient, an unknown template slug or language, a parameter mismatch, or a service message sent into a closed customer service window (`WhatsAppServiceWindowClosed`). An unfunded wallet is not one of them: the send is accepted, and the message ends `rejected` with `insufficient_balance` once Bird tries to charge it. Actual delivery happens asynchronously: the message moves to `sent` when we hand it to WhatsApp, then to a terminal status (`delivered` or `failed`) when the receipt arrives, reported through [events](/docs/guides/whatsapp/events/message-status), [webhooks](/docs/guides/webhooks), and the read endpoints. A read receipt is surfaced separately as a `read_at` timestamp and a `whatsapp.read` event rather than 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 it 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.

## Receiving the reply

Inbound messages land on the same resource as outbound ones, and every one of them resets the service window. [Receiving WhatsApp messages](/docs/guides/whatsapp/receiving-whatsapp) covers reading them over the API, fetching the media a contact sent, and the `whatsapp.received` webhook.

## 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). A message is charged in two steps, at two different moments, and the `cost` object on the message reports both:

| Field                | What it is                                                     | When it lands                                            |
| -------------------- | -------------------------------------------------------------- | -------------------------------------------------------- |
| `transaction_amount` | Bird's fee for handling the send                               | When Bird processes the accepted send, before dispatch   |
| `passthrough_amount` | Meta's share of the message price, which Bird passes through   | When an applicable `delivered` or `read` receipt arrives |
| `amount`             | The sum of the components priced so far                        | Grows as each component lands                            |
| `currency_code`      | Your organization wallet's currency, shared by both components | With the first component                                 |

Both amounts are decimal strings, net of tax.

> **Note:** a service message carries no Meta share until September 30th 2026, and neither does a utility template delivered inside an open customer service window. From October 1st 2026 Meta charges for both, except inside a 72-hour free entry point window, and with 1,000 free service messages per business phone number per month: [October 2026 pricing changes](/docs/knowledge-base/whatsapp/october-2026-pricing-changes).

The two components are priced on different inputs. Bird's fee uses the category of the template you sent and the recipient's country, which comes from the phone number's country code or, on a send addressed to a [business-scoped user ID](/docs/guides/whatsapp/business-scoped-user-ids), from the two-letter prefix on that ID. Meta's share uses the category Meta itself reports on the applicable receipt, which can differ from the template's: Meta can report authentication-international when its destination, business-location and eligibility rules apply. See [WhatsApp authentication-international rates](/docs/knowledge-base/whatsapp/authentication-international-rates).

What `cost` reads depends on how far the message has travelled:

- **At the `202`**, `cost` is null. Nothing has been priced.
- **After processing**, `transaction_amount` is set and `amount` equals it. `passthrough_amount` stays null.
- **After an applicable `delivered` or `read` receipt**, a successfully recorded Meta charge populates `passthrough_amount`, and `amount` reflects the recorded components.

A null component means no amount is recorded in that projection; it is not proof that the message was free. A component explicitly priced at zero reads `"0.00000"`.

The two charges also fail differently. Bird's fee fails closed: when it cannot go through after the `202` because the wallet cannot cover the send or the route has no configured price, the message ends `rejected` with the error code `insufficient_balance` or `price_not_found`, and nothing is charged. A `rejected` message never reached WhatsApp, which is what separates it from `failed`. Meta's share fails open: if the wallet is short or the rate is missing when the receipt arrives, the charge is skipped without reversing the observed message state. Your delivery is never held up by the second charge.

A message charged by Bird retains that outbound charge if delivery later fails. The Meta fee is processed from an applicable `delivered` or `read` callback when Meta reports regular pricing with a resolvable category and destination. Both callback paths use the same fee identity and rely on the billing service’s deduplication. Reconcile replayed receipts against billing records instead of treating the message projection as a permanent debit receipt. Service or free-entry pricing can make the Meta component zero; an unresolved component is not evidence that the message was free.

Use the billing ledger for financial reconciliation. Message `cost` fields are projections of the charges and can lag or remain incomplete. See [WhatsApp metrics](/docs/guides/whatsapp/tracking-and-metrics) for the distinction between message observations and billing records.

WhatsApp events do not carry cost. To read either component, read the message back with `GET /v1/whatsapp/messages/{id}`.

## Next steps

- [Service messages](/docs/guides/whatsapp/message-types): send text, media, and interactive content inside the customer service window
- [Templates](/docs/guides/whatsapp/templates): choose a template and supply its variables
- [Receiving WhatsApp messages](/docs/guides/whatsapp/receiving-whatsapp): read replies and fetch inbound media
- [Message status webhooks](/docs/guides/whatsapp/webhooks/message-status): receive delivery and failure updates
- [Idempotency](/docs/guides/idempotency): retry safely with the `Idempotency-Key` header

## Related resources

- [Connecting WhatsApp to Bird: from buying a number to a live channel](/learn/whatsapp/connecting-whatsapp-to-bird) (video)
- [What is the 24-hour customer service window on WhatsApp?](/explained/whatsapp/what-is-the-24-hour-customer-service-window) (answer)
- [WhatsApp message builder](/tools/whatsapp-message-builder) (tool)
- [WhatsApp](/whatsapp-api) (product)

[Get an implementation brief](/learn/workspace?topic=whatsapp)
