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 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.
Broadcasts work the same way on other messaging channels; the Broadcasts guide covers the shared parts. This page covers what is specific to WhatsApp.
#Before you send
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 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 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).
#Send a broadcast
#Create the draft
Call POST /v1/broadcasts with platform: "whatsapp", the profile and account, a name for your own reference and the template to send.
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 }
}'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"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 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 |
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, or pick a time with POST /v1/broadcasts/{broadcastId}/schedule and an ISO 8601 scheduledAt:
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 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 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}for totals (sentCount,deliveredCount,readCount,failedCount) and the broadcaststatus(draft,scheduled,sending,completed,failed,cancelled). - List people with
GET /v1/broadcasts/{broadcastId}/recipients, filtered bystatus. Failed recipients carryerrorCodeanderrorExplanationfrom 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 |
List broadcasts |
POST /v1/broadcasts |
Create broadcast draft |
GET /v1/broadcasts/{broadcastId} |
Get broadcast details |
PATCH /v1/broadcasts/{broadcastId} |
Update broadcast |
DELETE /v1/broadcasts/{broadcastId} |
Delete broadcast |
POST /v1/broadcasts/{broadcastId}/send |
Send broadcast now |
POST /v1/broadcasts/{broadcastId}/schedule |
Schedule broadcast for later |
POST /v1/broadcasts/{broadcastId}/cancel |
Cancel broadcast |
GET /v1/broadcasts/{broadcastId}/recipients |
List broadcast recipients |
POST /v1/broadcasts/{broadcastId}/recipients |
Add recipients to a broadcast |
#Webhook events
| Event | When it fires |
|---|---|
message.sent |
Message sent. |
message.delivered |
Message delivered. |
message.read |
Message read. |
message.failed |
Message delivery failed. |
whatsapp.account.quality_updated |
WhatsApp quality rating or messaging limit changed. |