Broadcasts
Bulk message broadcasts to a segment of contacts across one or more channels (WhatsApp/Telegram/Instagram). The group covers the whole lifecycle: creating a draft, publishing and running it, A/B variants, throughput and send-window control, opt-outs, click tracking, run management, and re-sending to failed recipients. Every endpoint requires an active subscription and the plan's broadcasts feature.
Lifecycle and scheduling
A broadcast can be a draft (is_draft: true) and later launched in one of these ways:
- immediately —
schedule.mode = now; - scheduled —
later+scheduledAt; - recurring — a
recurrencerule.
Lifecycle: draft → publish → run(s); each run tracks its own progress (sent/delivered/read/failed/skipped).
Broadcast status (status): draft → scheduled/active → completed or failed (cancelled on cancellation). For a one-off broadcast (no recurring schedule), failed is set when its run finishes with every recipient either failed or skipped — the same value is accepted by the statuses[] filter on GET /broadcasts. Re-sending to failed recipients (retry-failed) resets such a broadcast back to scheduled.
A/B variants
Per-channel content supports A/B variants (2–3 per channel, content.channels[].variants): a variant is assigned to each recipient deterministically, with a per-variant stats breakdown in stats.byVariant.
Throughput and send window
Throughput is governed by schedule.rateControl (messagesPerMinute, jitter, dailyLimit, skipContactedWithinHours) and by the sendWindow (the smart mode delivers to each recipient during their historically active hour).
Opt-out
Contacts with opted_out_at are always excluded from the audience; an inbound stop-word message (stop/unsubscribe) auto-opts the contact out.
Click tracking
When content.settings.trackClicks = true, links are rewritten to tracking /r/{code} URLs — a public, unauthenticated redirect that records the click and 302-redirects to the original.
Managing runs
Runs can be paused/resumed/cancelled (pending recipients → skipped) and failed recipients can be re-sent (retry-failed).
Template validation before sending
For content.channels[] entries with type: "template", an approved version of the selected template on that channel is verified before sending starts. The check runs at three entry points: POST /broadcasts (when schedule.mode is "now"), POST /{broadcast}/run, and POST /{broadcast}/publish. If no approved template is found, the request is rejected with 422 and errors.content[0] reads Channel "..." has no approved template "..." (language ru). Approve this template for the channel or pick a channel where it is approved.
AI copy helper
The AI text helper (POST /broadcasts/assist, generate/improve) shares a 60/hour rate limit with the rest of the AI assist.
Endpoints
| Method | Path | Summary |
|---|---|---|
| GET | /v1/broadcasts/metaBroadcast builder metadata (segments, variables, templates) | Broadcast builder metadata (segments, variables, templates) |
| GET | /v1/broadcastsList broadcasts (paginated, filterable by status) | List broadcasts (paginated, filterable by status) |
| GET | /v1/broadcasts/{broadcast}Get a broadcast | Get a broadcast |
| POST | /v1/broadcastsCreate a broadcast (supports is_draft) | Create a broadcast (supports is_draft) |
| PATCH | /v1/broadcasts/{broadcast}Update a broadcast | Update a broadcast |
| POST | /v1/broadcasts/{broadcast}Update a broadcast (POST alias of PATCH) | Update a broadcast (POST alias of PATCH) |
| DELETE | /v1/broadcasts/{broadcast}Delete a broadcast | Delete a broadcast |
| POST | /v1/broadcasts/estimateEstimate broadcast audience size | Estimate broadcast audience size |
| POST | /v1/broadcasts/{broadcast}/duplicateDuplicate a broadcast | Duplicate a broadcast |
| POST | /v1/broadcasts/{broadcast}/publishPublish a draft (launch it or schedule it) | Publish a draft (launch it or schedule it) |
| POST | /v1/broadcasts/{broadcast}/cancelCancel a one-off broadcast | Cancel a one-off broadcast |
| POST | /v1/broadcasts/{broadcast}/runManually trigger a broadcast run | Manually trigger a broadcast run |
| GET | /v1/broadcasts/{broadcast}/messagesRun recipients (paginated, filterable by status) | Run recipients (paginated, filterable by status) |
| GET | /v1/broadcasts/{broadcast}/runsList broadcast runs | List broadcast runs |
| GET | /v1/broadcasts/{broadcast}/runs/{run}Get a specific run | Get a specific run |
| POST | /v1/broadcasts/{broadcast}/pause-recurringPause a recurring schedule | Pause a recurring schedule |
| POST | /v1/broadcasts/{broadcast}/resume-recurringResume a recurring schedule | Resume a recurring schedule |
| POST | /v1/broadcasts/testSend a test message to a specific recipient | Send a test message to a specific recipient |
| POST | /v1/broadcasts/assistAI helper for broadcast copy (generate/improve) | AI helper for broadcast copy (generate/improve) |
| POST | /v1/broadcasts/{broadcast}/runs/{run}/pausePause a run | Pause a run |
| POST | /v1/broadcasts/{broadcast}/runs/{run}/resumeResume a run | Resume a run |
| POST | /v1/broadcasts/{broadcast}/runs/{run}/cancelCancel a run (pending recipients → skipped) | Cancel a run (pending recipients → skipped) |
| POST | /v1/broadcasts/{broadcast}/runs/{run}/retry-failedRe-send to failed recipients only | Re-send to failed recipients only |
Examples
Create and immediately launch a broadcast
Request
curl -X POST https://api.aisar.app/v1/broadcasts \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "February promo",
"is_draft": false,
"audience": { "segmentId": "12" },
"channels": { "mode": "specific", "channelIds": [7] },
"content": {
"channels": [
{ "type": "text", "channelId": 7, "accountType": "whatsapp", "text": "Hi {{first_name}}, 20% off this week!" }
]
},
"schedule": { "mode": "now" }
}'Response
{
"data": {
"id": 88,
"company_id": 1,
"name": "February promo",
"status": "active",
"audience": {
"conditions": [],
"group_logic": "and"
},
"channels": [{ "id": 7, "mode": "specific" }],
"content": [],
"schedule": { "mode": "now", "scheduledAt": null },
"recurring_schedule": null,
"runs": [
{ "id": 201, "broadcast_id": 88, "status": "processing", "total": 340, "sent": 0, "failed": 0, "delivered": 0, "read": 0, "started_at": "2026-03-15T10:00:00.000000Z", "completed_at": null }
],
"created_at": "2026-03-15T10:00:00.000000Z",
"updated_at": "2026-03-15T10:00:00.000000Z"
}
}Estimate broadcast audience
Request
curl -X POST https://api.aisar.app/v1/broadcasts/estimate \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"segment_id": 12,
"channel_ids": [7]
}'Response
{
"data": {
"total_contacts": 340,
"reachable_contacts": 312,
"excluded_contacts": 28,
"estimated_duration_seconds": 312,
"estimated_completion": "2026-03-15T10:05:12.000Z"
}
}Draft with A/B variants, rate control and a send window
Request
curl -X POST https://api.aisar.app/v1/broadcasts \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Spring reactivation",
"is_draft": true,
"audience": { "segmentId": "12" },
"channels": { "mode": "specific", "channelIds": [7] },
"content": {
"channels": [
{
"type": "text",
"channelId": 7,
"accountType": "whatsapp",
"text": "Hi {{first_name}}, we miss you!",
"variants": [
{ "key": "A", "text": "Hi {{first_name}}, we miss you — here is 15% off." },
{ "key": "B", "text": "{{first_name}}, come back for 15% off this week." }
]
}
],
"settings": {
"trackClicks": true,
"includeOptOut": true,
"optOutText": "Reply STOP to unsubscribe"
}
},
"schedule": {
"mode": "later",
"scheduledAt": "2026-04-01T09:00:00Z",
"rateControl": {
"enabled": true,
"messagesPerMinute": 30,
"jitterMinSeconds": 2,
"jitterMaxSeconds": 8,
"dailyLimit": 5000,
"skipContactedWithinHours": 48
},
"sendWindow": {
"enabled": true,
"mode": "smart",
"startTime": "09:00",
"endTime": "20:00",
"timezone": "recipient"
}
}
}'Response
{
"data": {
"id": 89,
"company_id": 1,
"name": "Spring reactivation",
"status": "draft",
"audience": { "conditions": [], "group_logic": "and" },
"channels": [{ "id": 7, "mode": "specific" }],
"content": [],
"schedule": { "mode": "later", "scheduledAt": "2026-04-01T09:00:00.000000Z" },
"recurring_schedule": null,
"runs": [],
"created_at": "2026-03-20T12:00:00.000000Z",
"updated_at": "2026-03-20T12:00:00.000000Z"
}
}Publish a draft (draft → scheduled)
Request
curl -X POST https://api.aisar.app/v1/broadcasts/89/publish \
-H "Authorization: Bearer YOUR_API_TOKEN"Response
{
"data": {
"id": 89,
"status": "scheduled",
"schedule": { "mode": "later", "scheduledAt": "2026-04-01T09:00:00.000000Z" },
"runs": [],
"updated_at": "2026-03-20T12:05:00.000000Z"
}
}Error: no approved template on the channel
Request
curl -X POST https://api.aisar.app/v1/broadcasts/90/run \
-H "Authorization: Bearer YOUR_API_TOKEN"Response
{
"data": {
"message": "Channel \"WhatsApp Main\" has no approved template \"spring_promo\" (language ru). Approve this template for the channel or pick a channel where it is approved.",
"errors": {
"content": [
"Channel \"WhatsApp Main\" has no approved template \"spring_promo\" (language ru). Approve this template for the channel or pick a channel where it is approved."
]
}
}
}Test message before launching
Request
curl -X POST https://api.aisar.app/v1/broadcasts/test \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 7,
"recipient": "+77000000001",
"content": { "type": "text", "text": "Test: 15% off this week!" }
}'Response
{
"data": {
"message": "Test message queued.",
"message_id": "b1f2c3d4-EXAMPLE"
}
}AI helper for broadcast copy
Request
curl -X POST https://api.aisar.app/v1/broadcasts/assist \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"action": "improve",
"text": "hey buy our stuff now big sale",
"locale": "en"
}'Response
{
"data": {
"text": "Big news, {{first_name}} — our best sale of the season is live. Save 20% this week only.",
"action": "improve",
"tokens_used": 128
}
}Run recipients (filtered by failed status)
Request
curl "https://api.aisar.app/v1/broadcasts/89/messages?status=failed&per_page=2" \
-H "Authorization: Bearer YOUR_API_TOKEN"Response
{
"data": [
{
"id": 7001,
"contact_id": 123,
"identifier": "+7700000XXXX",
"status": "failed",
"error_message": "recipient_not_on_whatsapp",
"attempts": 2,
"variant_key": "A",
"delivered_at": null,
"read_at": null,
"replied_at": null,
"clicked_at": null,
"contact": { "id": 123, "display_name": "Aylin Weber", "first_name": "Aylin", "last_name": "Weber" },
"channel": { "id": 7, "name": "WhatsApp Main" }
},
{
"id": 7002,
"contact_id": 124,
"identifier": "+7700000YYYY",
"status": "failed",
"error_message": "message_send_timeout",
"attempts": 3,
"variant_key": "B",
"delivered_at": null,
"read_at": null,
"replied_at": null,
"clicked_at": null,
"contact": { "id": 124, "display_name": "Marco Ricci", "first_name": "Marco", "last_name": "Ricci" },
"channel": { "id": 7, "name": "WhatsApp Main" }
}
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 2, "total": 2 }
}Re-send to failed recipients
Request
curl -X POST https://api.aisar.app/v1/broadcasts/89/runs/201/retry-failed \
-H "Authorization: Bearer YOUR_API_TOKEN"Response
{
"data": {
"id": 201,
"broadcast_id": 89,
"status": "processing",
"total": 340,
"sent": 300,
"failed": 0,
"delivered": 280,
"read": 190,
"started_at": "2026-04-01T09:00:00.000000Z",
"completed_at": null
}
}