# WhatsApp broadcasts

Send an approved template to a list of contacts with per-person variables, now or later, and track who received and read it.

A broadcast sends one approved [template](https://pozy.ai/docs/whatsapp/templates) to many people, each as a private one-to-one message with their own name, order number or code filled in. Use it for campaigns, reminders and announcements. Pozy queues the sends, tracks each recipient through sent, delivered and read, and puts any replies in the [inbox](https://pozy.ai/docs/inbox/messages).

Broadcasts work the same way on other messaging channels; the [Broadcasts guide](https://pozy.ai/docs/automations/broadcasts) covers the shared parts. This page covers what is specific to WhatsApp.

## Before you send

> **Only message people who opted in:**
> 
> WhatsApp requires that every recipient has agreed to hear from your business on WhatsApp. Sending to people who did not opt in leads to blocks and reports, which lower your number's quality rating and can get templates paused or the number restricted. Keep `isSubscribed` accurate on your [contacts](https://pozy.ai/docs/inbox/contacts) and filter on it.

Three limits shape a WhatsApp broadcast:

| Limit | What happens |
| --- | --- |
| Messaging limit tier | Meta caps the unique people a number can reach per day. A new number is on `TIER_250`. Once the cap is reached Meta stops delivering, so a list bigger than the tier will not all arrive. Check `messaging_limit_tier` with [number info](https://pozy.ai/docs/api/getWhatsAppNumberInfo) before a large send. |
| Quality rating | A `RED` rating can pause templates and lower the tier. Watch `whatsapp.account.quality_updated`. |
| Throughput | A coexistence number sends at most 20 messages per second. Cloud API-only numbers follow Meta's standard throughput. |

Each recipient counts as one outbound message on your Pozy bill. Meta also charges its own per-template fee to your WABA ([Pricing](https://pozy.ai/docs/whatsapp/pricing)).

## Send a broadcast

### Create the draft

Call [`POST /v1/broadcasts`](https://pozy.ai/docs/api/createBroadcast) with `platform: "whatsapp"`, the profile and account, a `name` for your own reference and the `template` to send.

```bash cURL
curl -X POST https://api.pozy.ai/v1/broadcasts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6710c4e2b9a1f03d8e5c7a21",
    "accountId": "6712a9d4c0e3b58f1a7d2e64",
    "platform": "whatsapp",
    "name": "November loyalty offer",
    "template": {
      "name": "loyalty_offer",
      "language": "en",
      "components": [{
        "type": "body",
        "parameters": [
          { "type": "text", "text": "{{1}}" },
          { "type": "text", "text": "{{2}}" }
        ]
      }],
      "variableMapping": {
        "1": { "field": "name" },
        "2": { "field": "custom", "customValue": "BREAD10" }
      }
    },
    "segmentFilters": { "tags": ["loyalty"], "isSubscribed": true }
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/broadcasts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    profileId: "6710c4e2b9a1f03d8e5c7a21",
    accountId: "6712a9d4c0e3b58f1a7d2e64",
    platform: "whatsapp",
    name: "November loyalty offer",
    template: {
      name: "loyalty_offer",
      language: "en",
      components: [{
        type: "body",
        parameters: [{ type: "text", text: "{{1}}" }, { type: "text", text: "{{2}}" }],
      }],
      variableMapping: {
        "1": { field: "name" },
        "2": { field: "custom", customValue: "BREAD10" },
      },
    },
    segmentFilters: { tags: ["loyalty"], isSubscribed: true },
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

const broadcastId = data.broadcast.id; // status "draft"
```
```python Python
import os
import requests

H = {"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"}
resp = requests.post(
    "https://api.pozy.ai/v1/broadcasts",
    headers=H,
    json={
        "profileId": "6710c4e2b9a1f03d8e5c7a21",
        "accountId": "6712a9d4c0e3b58f1a7d2e64",
        "platform": "whatsapp",
        "name": "November loyalty offer",
        "template": {
            "name": "loyalty_offer",
            "language": "en",
            "components": [{
                "type": "body",
                "parameters": [{"type": "text", "text": "{{1}}"}, {"type": "text", "text": "{{2}}"}],
            }],
            "variableMapping": {
                "1": {"field": "name"},
                "2": {"field": "custom", "customValue": "BREAD10"},
            },
        },
        "segmentFilters": {"tags": ["loyalty"], "isSubscribed": True},
    },
    timeout=30,
)
broadcast_id = resp.json()["broadcast"]["id"]
```

A template without variables only needs `name` and `language`.

### Add recipients

Call [`POST /v1/broadcasts/{broadcastId}/recipients`](https://pozy.ai/docs/api/addBroadcastRecipients) with one of:

| Field | Adds |
| --- | --- |
| `phones` | Numbers in E.164 (`+351912345678`). Pozy creates a contact for any number it has not seen. |
| `contactIds` | Existing Pozy contacts, by contact id |
| `useSegment: true` | Every contact matching the broadcast's `segmentFilters` |

```bash cURL
curl -X POST "https://api.pozy.ai/v1/broadcasts/$BROADCAST_ID/recipients" \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "useSegment": true }'
```

The response says how many were `added` and how many were `skipped` (duplicates, and contacts with no WhatsApp channel).

### Send now or later

Start right away with [`POST /v1/broadcasts/{broadcastId}/send`](https://pozy.ai/docs/api/sendBroadcast), or pick a time with [`POST /v1/broadcasts/{broadcastId}/schedule`](https://pozy.ai/docs/api/scheduleBroadcast) and an ISO 8601 `scheduledAt`:

```bash cURL
curl -X POST "https://api.pozy.ai/v1/broadcasts/$BROADCAST_ID/schedule" \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scheduledAt": "2026-11-05T09:30:00Z" }'
```

A scheduled broadcast can be stopped with [`POST /v1/broadcasts/{broadcastId}/cancel`](https://pozy.ai/docs/api/cancelBroadcast) before it starts.

## Fill variables per recipient

`variableMapping` maps each numbered variable of the template body to a value. Pozy resolves it for every recipient at send time.

| `field` | Becomes |
| --- | --- |
| `name` | The contact's name, or "there" if it is empty |
| `phone` | The recipient's phone number |
| `email` | The contact's email |
| `company` | The contact's company |
| `custom` | The fixed `customValue` you set, the same for everyone |

Broadcasts fill numbered variables (`{{1}}`, `{{2}}`) only, not named ones. The number of `variableMapping` entries must match the number of variables in the template body exactly. If it does not, Meta rejects each send with code `132000` and every recipient fails.

`segmentFilters.customFields` matches contact [custom fields](https://pozy.ai/docs/inbox/custom-fields) by slug, and the type must match too: `5` does not match `"5"`.

## Track delivery

Each recipient moves through `pending`, `sent`, `delivered` and `read`, or ends at `failed`. Two ways to follow along:

- Poll [`GET /v1/broadcasts/{broadcastId}`](https://pozy.ai/docs/api/getBroadcast) for totals (`sentCount`, `deliveredCount`, `readCount`, `failedCount`) and the broadcast `status` (`draft`, `scheduled`, `sending`, `completed`, `failed`, `cancelled`).
- List people with [`GET /v1/broadcasts/{broadcastId}/recipients`](https://pozy.ai/docs/api/listBroadcastRecipients), filtered by `status`. Failed recipients carry `errorCode` and `errorExplanation` from WhatsApp.

Or subscribe to `message.delivered`, `message.read` and `message.failed` and update your own records as events arrive.

Replies come in as normal conversations. Because the customer wrote back, the 24-hour window is open and you can answer with free-form messages.

## Common errors

| Error | Cause | Fix |
| --- | --- | --- |
| `400` "Broadcast has no recipients" on send or schedule | Nothing was added | Add recipients first |
| `400` "Cannot send broadcast in sending status" | The broadcast already started or finished | Check `status` before retrying |
| Many recipients `failed` with `132000` | Variable count does not match the template | Fix `variableMapping` and create a new broadcast |
| Far fewer deliveries than recipients on a large list | The number's messaging limit tier was reached | Wait for Meta to raise the tier, or spread the list over several days |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/broadcasts`](https://pozy.ai/docs/api/listBroadcasts) | List broadcasts |
| [`POST /v1/broadcasts`](https://pozy.ai/docs/api/createBroadcast) | Create broadcast draft |
| [`GET /v1/broadcasts/{broadcastId}`](https://pozy.ai/docs/api/getBroadcast) | Get broadcast details |
| [`PATCH /v1/broadcasts/{broadcastId}`](https://pozy.ai/docs/api/updateBroadcast) | Update broadcast |
| [`DELETE /v1/broadcasts/{broadcastId}`](https://pozy.ai/docs/api/deleteBroadcast) | Delete broadcast |
| [`POST /v1/broadcasts/{broadcastId}/send`](https://pozy.ai/docs/api/sendBroadcast) | Send broadcast now |
| [`POST /v1/broadcasts/{broadcastId}/schedule`](https://pozy.ai/docs/api/scheduleBroadcast) | Schedule broadcast for later |
| [`POST /v1/broadcasts/{broadcastId}/cancel`](https://pozy.ai/docs/api/cancelBroadcast) | Cancel broadcast |
| [`GET /v1/broadcasts/{broadcastId}/recipients`](https://pozy.ai/docs/api/listBroadcastRecipients) | List broadcast recipients |
| [`POST /v1/broadcasts/{broadcastId}/recipients`](https://pozy.ai/docs/api/addBroadcastRecipients) | Add recipients to a broadcast |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`message.sent`](https://pozy.ai/docs/webhooks/events#message-sent) | Message sent. |
| [`message.delivered`](https://pozy.ai/docs/webhooks/events#message-delivered) | Message delivered. |
| [`message.read`](https://pozy.ai/docs/webhooks/events#message-read) | Message read. |
| [`message.failed`](https://pozy.ai/docs/webhooks/events#message-failed) | Message delivery failed. |
| [`whatsapp.account.quality_updated`](https://pozy.ai/docs/webhooks/events#whatsapp-account-quality-updated) | WhatsApp quality rating or messaging limit changed. |
