Create and send a template broadcast
POST/api/v1/broadcasts
scope
broadcasts:sendIdempotentCosts 5 rate-limit unitsCreates a WhatsApp template campaign, materialises its recipients, and starts sending — immediately, or at schedule_at if that is in the future. Always asynchronous: the response is 202 with the campaign, and GET /broadcasts/{id} reports progress. Per-recipient values come from variables (a placeholder → static text, a contact field, or a custom field). Counts toward the account’s monthly broadcast limit; a phone-number audience that creates contacts counts toward the contact limit.
Body
application/json| Name | Type | Description |
|---|---|---|
namerequired | string | Campaign name shown in the dashboard |
templaterequired | object | |
template.namerequired | string | An APPROVED template on this account |
template.languageoptional | string | Defaults to en_US |
audiencerequired | one of | Who receives it. Opted-out and phone-less contacts are always skipped. |
audience (option 1)optional | object | |
audience.typerequired | string | phones |
audience.phonesrequired | string[] | Numbers to reach; unknown ones become contacts |
audience (option 2)optional | object | |
audience.typerequired | string | contacts |
audience.contact_idsrequired | string (uuid)[] | |
audience (option 3)optional | object | |
audience.typerequired | string | tags |
audience.tag_idsrequired | string (uuid)[] | Contacts carrying ANY of these tags |
audience.exclude_tag_idsoptional | string (uuid)[] | |
audience (option 4)optional | object | |
audience.typerequired | string | all |
audience.exclude_tag_idsoptional | string (uuid)[] | |
audience (option 5)optional | object | |
audience.typerequired | string | segment |
audience.segment_idrequired | string (uuid) | UUID |
audience.exclude_tag_idsoptional | string (uuid)[] | |
variablesoptional | map<string, object> | Placeholder key ("1", "2", …) → how to fill it |
header_media_urloptional | string (url) | Overrides the template’s header media |
schedule_atoptional | string (date-time) | Start later; omit to start now |
Request
curl -X POST "https://your-crm.example.com/api/v1/broadcasts" \
-H "Authorization: Bearer $RAABTA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a9e-3b4d-4c5e-8f7a-9b0c1d2e3f4a" \
-d '{
"name": "September reminders",
"template": {
"name": "appointment_reminder",
"language": "en_US"
},
"audience": {
"type": "tags",
"tag_ids": [
"1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
]
},
"variables": {
"1": {
"type": "field",
"value": "name"
},
"2": {
"type": "static",
"value": "Saturday 6 September"
},
"3": {
"type": "static",
"value": "11:00"
}
}
}'Response · 202 Accepted
{
"data": {
"id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c0d",
"name": "September reminders",
"kind": "template",
"channel": "whatsapp",
"template": {
"name": "appointment_reminder",
"language": "en_US"
},
"status": "scheduled",
"scheduled_at": "2026-09-02T10:00:00.000Z",
"counts": {
"total": 240,
"sent": 0,
"delivered": 0,
"read": 0,
"replied": 0,
"failed": 0
},
"created_at": "2026-09-02T10:00:00.000Z",
"updated_at": "2026-09-02T10:02:00.000Z"
}
}Response
Wrapped in { data: … }| Name | Type | Description |
|---|---|---|
idrequired | string (uuid) | UUID |
namerequired | string | Campaign name, as shown in the dashboard |
kindrequired | string (enum) | template = an approved WhatsApp template; free_text = a personal-WhatsApp messagetemplatefree_text |
channelrequired | string | whatsapp | personal_whatsapp |
templaterequired | object | null | The template a template broadcast sends |
template.namerequired | string | |
template.languagerequired | string | |
statusrequired | string (enum) | draft → scheduled → sending → sent | faileddraftscheduledsendingsentfailed |
scheduled_atrequired | string (date-time) | null | When it was (or will be) started |
countsrequired | object | Live delivery counters — poll this endpoint for progress |
counts.totalrequired | integer | Recipients materialised |
counts.sentrequired | integer | Handed to the channel |
counts.deliveredrequired | integer | Confirmed delivered to the handset |
counts.readrequired | integer | Confirmed read |
counts.repliedrequired | integer | Recipients who replied |
counts.failedrequired | integer | Rejected by the channel |
created_atrequired | string (date-time) | ISO 8601 timestamp |
updated_atrequired | string (date-time) | ISO 8601 timestamp |
Errors
| Status | Code | When |
|---|---|---|
| 400 | bad_request | The request could not be parsed: malformed JSON, an invalid cursor, or a query parameter of the wrong shape. |
| 400 | validation_error | The body or query failed validation. `details` lists each failing field with a `path` and a `message`. |
| 401 | unauthorized | No usable API key: the Authorization header is missing or malformed, or the key is unknown, revoked or expired. The three are deliberately indistinguishable. |
| 402 | plan_limit_reached | The account has reached a plan limit for this resource. `details[0].limit` is the cap; ask the account owner to raise it. |
| 403 | forbidden | The key is valid but lacks the scope this endpoint requires, or the request came from an address outside the key’s IP allowlist. The message says which. |
| 403 | account_suspended | The account this key belongs to is suspended. Rotating the key will not help; contact support. |
| 409 | conflict | The request conflicts with current state: a duplicate phone number on create, an invalid state transition, or an idempotent request that is still in flight. |
| 422 | unprocessable | The request was well-formed but cannot be carried out. `reason` is a stable string saying why (for example `outside_window` or `stage_not_in_pipeline`). |
| 422 | channel_not_connected | The channel this request needs is not connected on the account. Connect it in Settings and retry. |
| 429 | rate_limited | The per-key budget, or the per-IP budget for failed authentication, is exhausted. Honour `Retry-After` before retrying. |
| 500 | internal | Something failed on our side. Safe to retry with the same Idempotency-Key; quote `request_id` if it persists. |