
# Broadcasts API

A **broadcast** is one outbound send: an audience, an opening message, one channel, and a schedule. Optionally it also names the AI Agent that handles the replies it gets back. The Broadcasts API lets you build, price, launch and monitor those sends from your own code instead of the dashboard. For the product itself, see the [Broadcasts guide](../broadcasts/broadcasts.md).

- **Base URL** — `https://api.youraiconnector.com/v1`
- **Authentication** — your API key (see [Authentication](authentication.md))
- **Errors & paging** — see [Errors & Pagination](errors-and-pagination.md)

All examples below show the `?apiKey=` query form in cURL and the `X-API-Key` header in JavaScript and Python — either works on every endpoint.

> **In the API explorer.** Every endpoint on this page is in the published OpenAPI specification, so you can browse its exact fields and run live requests in the [API explorer](reference.md).


---

## How a send is put together

Sending a broadcast is four calls, not one:

1. **Create** the broadcast with its audience, channel and schedule — it starts as a `Draft`.
2. **Set the opening message.** On WhatsApp Business that means submitting a template for approval (or picking one you already had approved). On every other channel it is plain text.
3. **Estimate the cost** if you want to check the price before spending anything (optional).
4. **Launch it.** Launching runs a full check — audience, message, template approval, connected sender — and either starts the send or tells you exactly what is missing.

Nothing is sent until you call launch.

---

## The broadcast object

```json
{
  "id": "bcd123abc456",
  "name": "June promo",
  "status": "Draft",
  "channel": "whatsapp",
  "agent_id": "agt_789",
  "list_id": "lst_456",
  "list_name": "Newsletter subscribers",
  "total_contacts": 240,
  "send_to_new_list_members": false,
  "whats_app_template": {
    "body": "Hi {{first_name}}, our June offer is live.",
    "status": "approved",
    "sid": "HX0123...",
    "language": "en",
    "category": "marketing",
    "variables": ["first_name"]
  },
  "execution_date": 1781000000000,
  "drip_mode": true,
  "time_critical": false,
  "total_contacts_sent": 0,
  "credits_used": 0,
  "created_at": 1780900000000,
  "last_modified_at": 1780900000000
}
```

**Timestamps come back as epoch milliseconds** (`execution_date`, `created_at`, `last_modified_at`, …), and any contact reference comes back as a path string like `contacts/uid_whatsapp_15551234567`.

### Fields you set

| Field | Description |
|---|---|
| `name` | What the broadcast is called in the dashboard. |
| `channel` | The one channel this broadcast sends on: `whatsapp`, `whatsapp_web`, `sms`, `instagram`, `messenger`, `facebook`, `telegram`, `instagram_private`, `line`, `viber`, `imessage`, `email`, `chat_widget`, `custom_channel`. A broadcast has exactly one channel — to send the same thing elsewhere, [duplicate it onto another channel](#duplicate-a-broadcast). `tiktok` and `skool` are reply-only and can never be broadcast on. |
| `agent_id` | The AI Agent that answers replies. Leave it `null` and replies land in your team inbox instead. |
| `list_id` | The contact list to send to. This is how you set the audience from the API — see [Contacts](contacts.md) for creating and filling lists. |
| `list_name` | Display name shown next to the broadcast. Cosmetic. |
| `send_to_new_list_members` | `true` keeps the broadcast armed so anyone added to the list later also gets the opener. |
| `whats_app_template` | The opening message. On WhatsApp Business it is a real approved template; on every other channel its `body` is used as the plain opening text. Set it through the [template endpoints](#the-opening-message), not by hand. |
| `opener_media` | One image or video sent with the opener. Always send the whole object (or `null` to remove it) — writing individual keys inside it is rejected. Not supported on SMS. |
| `execution_date` | When to send. Send an ISO 8601 timestamp or epoch milliseconds. A future date schedules the send; omit it (or use a past one) to send as soon as you launch. |
| `drip_mode` | `true` paces the send in batches over time instead of all at once. |
| `time_critical` | `true` opts out of the automatic pacing that kicks in above 50 contacts — for a warm audience that needs the message now. It does not lift the channel's own daily sending limit. |
| `batch_size` | How many contacts per batch when dripping. |
| `follow_up_config` | The follow-up chain for contacts who never reply. |

Anything you send as `user_id`, `id`, `status` or `source_campaign_id` is ignored on create and dropped on update — the status only ever moves through the launch, pause and resume endpoints below.

### Fields the platform maintains

`status`, `total_contacts_sent`, `unique_contacts_replied`, `overall_reply_rate`, `credits_used`, `paused_reason`, `completion_summary`, the batch counters, and `contacts` (the individual contacts attached from the dashboard, read back as path strings). Read them, don't write them.

### Statuses

| Status | Meaning |
|---|---|
| `Draft` | Being built. Nothing is scheduled. |
| `Pending Approval` | Launched, but its WhatsApp template is still awaiting a decision. It starts sending on its own once the template is approved — you do not need to launch again. |
| `Scheduled` | Launched with a future `execution_date`. |
| `Sending` | Actively sending (a broadcast armed for new list members stays here while it waits for them). |
| `Paused` | Held — by you, or automatically by a safety check. |
| `Sent` | Finished. |
| `Failed` | Finished with more than half the sends failing. |

---

## Create a broadcast

`POST /broadcasts` — creates a `Draft`.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/broadcasts?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "June promo",
    "channel": "whatsapp",
    "list_id": "lst_456",
    "agent_id": "agt_789",
    "drip_mode": true,
    "execution_date": "2026-06-15T09:00:00.000Z"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/broadcasts", {
  method: "POST",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "June promo",
    channel: "whatsapp",
    list_id: "lst_456",
    agent_id: "agt_789",
    drip_mode: true,
    execution_date: "2026-06-15T09:00:00.000Z",
  }),
});
const { broadcast_id } = await res.json();
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/broadcasts",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "name": "June promo",
        "channel": "whatsapp",
        "list_id": "lst_456",
        "agent_id": "agt_789",
        "drip_mode": True,
        "execution_date": "2026-06-15T09:00:00.000Z",
    },
)
print(res.json()["broadcast_id"])
```

**Response** (`201`)

```json
{ "success": true, "broadcast_id": "bcd123abc456" }
```

---

## List broadcasts

`GET /broadcasts` — every broadcast on the account, newest first.

**Query parameters**

| Parameter | Required | Description |
|---|---|---|
| `status` | No | Return only broadcasts in one status, e.g. `Sending`. Match the spelling in the [status table](#statuses) exactly. |

```bash
curl "https://api.youraiconnector.com/v1/broadcasts?apiKey=YOUR_API_KEY&status=Sending"
```

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/broadcasts?status=Sending", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { broadcasts } = await res.json();
```

```python
res = requests.get(
    "https://api.youraiconnector.com/v1/broadcasts",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"status": "Sending"},
)
broadcasts = res.json()["broadcasts"]
```

**Response** (`200`)

```json
{ "success": true, "broadcasts": [{ "id": "bcd123abc456", "name": "June promo", "status": "Sending", "...": "..." }] }
```

---

## Get a broadcast

`GET /broadcasts/{broadcastId}` — returns `{ "success": true, "broadcast": { ... } }`. Use it to poll a running send: `total_contacts_sent`, `unique_contacts_replied`, `overall_reply_rate` and `credits_used` update as it goes.

```bash
curl "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456?apiKey=YOUR_API_KEY"
```

A broadcast that doesn't exist on your account returns `404`.

---

## Update a broadcast

`PUT /broadcasts/{broadcastId}` — send only the fields you want to change. You can also address a single key inside a nested object with a dotted path, e.g. `"whats_app_template.body"`.

```bash
curl -X PUT "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "June promo (v2)", "execution_date": "2026-06-16T09:00:00.000Z" }'
```

```javascript
await fetch("https://api.youraiconnector.com/v1/broadcasts/bcd123abc456", {
  method: "PUT",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ name: "June promo (v2)", execution_date: "2026-06-16T09:00:00.000Z" }),
});
```

An empty body returns `400`. Two rules worth knowing:

- **`opener_media` is all-or-nothing.** Send the complete object, or `null` to remove the attachment. A dotted path into it (`opener_media.name`) is rejected with `400`, because a half-updated attachment would describe a file that isn't there.
- **Status is not editable.** Use [launch](#launch-a-broadcast), [pause](#pause-and-resume), and [resume](#pause-and-resume).

---

## The opening message

Every broadcast carries its opener in `whats_app_template`. What that means depends on the channel:

- **WhatsApp Business** — it must be a template WhatsApp has approved. Use one of the two endpoints below.
- **Every other channel** (WhatsApp Web, SMS, Instagram, Messenger, Telegram, …) — the same field's `body` is simply the text that gets sent. Submitting it through the endpoint below stores it and marks it ready without involving WhatsApp at all.

### Submit a template for approval

`POST /broadcasts/{broadcastId}/template`

| Field | Required | Description |
|---|---|---|
| `body` | Yes | The message text, up to 1024 characters. Use `{{variable}}` placeholders for personalisation. |
| `name` | No | Template name. Defaults to the broadcast's name. |
| `language` | No | Language code. Defaults to `en`. |
| `category` | No | `marketing` (default), `utility`, `authentication`, or `authentication-international`. This is what the send is priced at, so keep it honest. |
| `variables` | No | The placeholder names, in the order they appear. Leave it out and they are read from the body — which is usually what you want, because the send fills them from each contact. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/template?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Hi {{first_name}}, our June offer is live until Friday.",
    "language": "en",
    "category": "marketing"
  }'
```

**Response** (`200`)

```json
{ "success": true, "broadcast_id": "bcd123abc456", "template_status": "pending", "template_sid": "HX0123..." }
```

`template_status` is what WhatsApp says: `pending` while it is being reviewed, `approved` when it is usable, `rejected` if it was refused. On a non-WhatsApp channel it comes straight back as `approved` with `template_sid: null` — nothing to review.

Things that will stop you:

- Submitting while a previous template is still under review returns `400`. Wait for the decision first.
- Editing a template that is currently approved keeps the approved one live until the new one comes back, so a running broadcast never loses its opener.
- On a WhatsApp number connected directly through Meta, a broadcast with an image or video attached cannot be submitted (`400`) — attachments are supported on the managed WhatsApp Business lane and on WhatsApp Web.

### Use a template you already had approved

`POST /broadcasts/{broadcastId}/template/select` — copies an already-approved template from your [template library](templates.md) onto the broadcast, so there is nothing to wait for.

| Field | Required | Description |
|---|---|---|
| `template_id` | Yes | The id of an approved template on your account. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/template/select?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "template_id": "tpl_abc123" }'
```

**Response** (`200`)

```json
{
  "success": true,
  "broadcast_id": "bcd123abc456",
  "template_status": "approved",
  "template_sid": "HX0123...",
  "body": "Hi {{first_name}}, our June offer is live until Friday.",
  "name": "june_promo",
  "language": "en",
  "variables": ["first_name"],
  "category": "marketing"
}
```

The approval is verified on our side from the library record — you only ever send the id. You get a `400` if the broadcast isn't a WhatsApp draft, if the template isn't approved, if it is a follow-up template rather than an opener, or if the broadcast has an attachment (library templates are text-only). A template id that isn't on your account returns `404`.

---

## Estimate the cost

`POST /broadcasts/{broadcastId}/estimate-cost` — prices the send before you commit to it. Available on `whatsapp` and `sms` broadcasts; any other channel returns `400`. The broadcast needs a `list_id`, since the estimate counts the audience.

```bash
curl -X POST "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/estimate-cost?apiKey=YOUR_API_KEY"
```

**WhatsApp response** (`200`) — credits, broken down by destination country:

```json
{
  "success": true,
  "channel": "whatsapp",
  "billing_mode": "credits",
  "data": {
    "countries": [
      { "countryCode": "31", "name": "Netherlands", "iso": "NL", "flag": "🇳🇱", "contactCount": 180, "costPerContact": 1.2, "subtotal": 216 },
      { "countryCode": "1", "name": "United States", "iso": "US", "flag": "🇺🇸", "contactCount": 60, "costPerContact": 0.9, "subtotal": 54 }
    ],
    "totalContacts": 240,
    "totalTemplateCost": 270,
    "templateCategory": "marketing",
    "billing_mode": "credits",
    "service_messages_billable_soon": false
  }
}
```

**SMS response** (`200`) — US dollars, based on live Twilio pricing for your own Twilio account:

```json
{
  "success": true,
  "channel": "sms",
  "billing_mode": "twilio_direct",
  "data": {
    "totalContacts": 240,
    "messageLength": 118,
    "segmentsPerMessage": 1,
    "totalSegments": 240,
    "estimatedCostUsd": 1.788,
    "priceUnit": "USD",
    "billedByTwilio": true,
    "billing_mode": "twilio_direct",
    "service_messages_billable_soon": false
  }
}
```

**Read `billing_mode` before you show a number.** It tells you who is being billed:

| `billing_mode` | Who pays | What the figures mean |
|---|---|---|
| `credits` | Your <span data-t="appName">Your AI Connector</span> account | `totalTemplateCost` and the per-country figures are credits. |
| `twilio_direct` | Your own Twilio account | `estimatedCostUsd` is what Twilio will charge you. |
| `meta_waba_direct` | Your own WhatsApp Business Account, billed by Meta | Every credit figure comes back `null` — deliberately, so it is never mistaken for "free". The country and contact counts are still accurate. |

SMS with no Twilio credentials connected still returns the segment counts, with `estimatedCostUsd: 0` — there is no pricing to look up.

---

## Launch a broadcast

`POST /broadcasts/{broadcastId}/launch`

Launching checks everything first and only then moves the broadcast forward. There is no partial launch: either it starts, or nothing changes and you get an error saying why.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/launch?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/launch", {
  method: "POST",
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
if (!data.success) console.error(data.error);
```

**Python**

```python
res = requests.post(
    "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/launch",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json())
```

**Response** (`200`)

```json
{ "success": true, "broadcast_id": "bcd123abc456", "status": "Scheduled" }
```

`status` is where the broadcast landed:

- `Scheduled` — `execution_date` is in the future.
- `Sending` — it started now.
- `Pending Approval` — the WhatsApp template is still under review. It sends itself as soon as the template is approved; do not call launch again.

Only a `Draft` (or a `Pending Approval` broadcast whose template has since been approved) can be launched — anything else returns `400`.

### Why a launch is refused

Every one of these comes back as `400` with a plain-language `error` message:

| Problem | What to fix |
|---|---|
| No audience | Set `list_id` (or attach contacts) before launching. |
| No opening message | Set the opener — see [The opening message](#the-opening-message). |
| Attachment on SMS | SMS cannot carry an image or video. Remove the attachment or move the broadcast to WhatsApp. |
| Attachment doesn't match the approved template | On WhatsApp the media lives inside the approved template, so swapping the attachment afterwards means resubmitting the template. |
| Template rejected | Rewrite the message and submit it again. |
| Template never submitted | Submit it (or select an approved one) first. |
| Template approved but missing from your WhatsApp account | Usually a template approved before the number finished connecting. Submit it again. |
| No connected sender for the channel | Connect the channel first — see [Channels](channels.md). |
| Reply-only channel | TikTok and Skool don't allow a business to start a conversation, so they can't be broadcast on. |
| Already armed | The broadcast already has a send scheduled. Pause it before launching again. |
| Still awaiting approval | It will send itself when the template is approved. |
| WhatsApp Business Account blocked by Meta | Meta has stopped business-initiated conversations on your own WhatsApp Business Account — normally a payment-method problem. Fix it in Meta's Business Manager. |
| Started from a classic campaign | Launch it from the campaign editor instead. See [classic campaigns in Broadcasts](#broadcasts-that-mirror-a-classic-campaign). |

---

## Pause and resume

`POST /broadcasts/{broadcastId}/pause` stops a `Sending` or `Scheduled` broadcast and tears down anything queued.

```bash
curl -X POST "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/pause?apiKey=YOUR_API_KEY"
```

Pausing a `Pending Approval` broadcast puts it back to `Draft` instead — nothing was scheduled yet, so there is nothing to resume into. Any other status returns `400`.

`POST /broadcasts/{broadcastId}/resume` restarts a `Paused` broadcast:

```bash
curl -X POST "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/resume?apiKey=YOUR_API_KEY"
```

**Response** (`200`)

```json
{ "success": true, "broadcast_id": "bcd123abc456" }
```

It resumes into `Sending`, or back into `Scheduled` if its `execution_date` is still in the future. Only a `Paused` broadcast can be resumed.

---

## Keep sending after a low-engagement pause

`POST /broadcasts/{broadcastId}/override-engagement-guard`

While a broadcast sends in batches, we measure how many people replied to each batch before starting the next one. If almost nobody is replying, the broadcast pauses itself — a send that keeps pushing into silence is the quickest way to get a number filtered or blocked. It's the **Continue anyway** button in the dashboard.

Because the reply rate that caused the pause cannot change while the broadcast is stopped, a plain [resume](#pause-and-resume) would just get paused again by the next check. This endpoint is the decision to keep going anyway: it records the override on that one broadcast, and lifts the pause in the same call if the broadcast was paused for low engagement.

```bash
curl -X POST "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/override-engagement-guard?apiKey=YOUR_API_KEY"
```

**Response** (`200`)

```json
{ "success": true, "broadcast_id": "bcd123abc456", "status": "Sending", "resumed": true }
```

- `resumed: true` — the broadcast was paused for low engagement and is now running again; `status` is where it resumed to.
- `resumed: false` — nothing was lifted, the override is simply recorded for future checks. That is what you get if the broadcast was never paused, or was paused for a different reason (you paused it by hand, a sending limit was hit, or too many sends errored). Those pauses are not lifted here — resume it yourself once you've dealt with the cause.

The override applies to this broadcast only. It is not an account setting, and it is safe to call twice.

---

## Duplicate a broadcast

`POST /broadcasts/{broadcastId}/duplicate` — copies the audience, message and settings into a new `Draft`. Everything about the previous run (counters, batches, schedule, reply stats) starts fresh.

| Field | Required | Description |
|---|---|---|
| `to_channel` | No | Create the copy on a different channel. This is how you send the same thing on two channels — a broadcast only ever has one. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456/duplicate?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to_channel": "sms" }'
```

**Response** (`201`)

```json
{ "success": true, "broadcast_id": "bcd999new111", "source_broadcast_id": "bcd123abc456" }
```

A copy never inherits a live WhatsApp approval: on a WhatsApp copy the template comes across needing your confirmation, and on a copy to another channel it is dropped and the text becomes the plain opener. Copying to SMS also drops any attachment, since SMS can't send one.

---

## Delete a broadcast

`DELETE /broadcasts/{broadcastId}`

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/broadcasts/bcd123abc456?apiKey=YOUR_API_KEY"
```

A `Sending` or `Scheduled` broadcast is refused with `400` — pause it first.

---

## Broadcasts that mirror a classic campaign

Classic campaigns that send messages also appear in Broadcasts, and the API returns them alongside native broadcasts (they carry a `source_campaign_id`). They behave a little differently, because the campaign remains in charge:

- **Editing** the audience, message or schedule works and is written through to the campaign.
- **Channel, reply Agent, attachment and all run counters are read-only** here — `400` if you try to change them. Change them on the campaign.
- **Launch** returns `400` pointing you to the campaign editor.
- **Pause and resume** work and act on the campaign.
- **Delete** returns `400` — delete the campaign instead, and its Broadcasts entry goes with it.
- **Duplicate** gives you an independent native broadcast, which is the supported way to move a proven campaign across.

---

## Errors

Failed requests return `{"success": false, "error": "<message>"}` with these statuses:

| Status | Meaning |
|---|---|
| `400` | Something about the request or the broadcast's state is wrong — a missing field, an invalid attachment, or a launch/pause/resume/delete that isn't allowed in the broadcast's current status. The `error` message names the reason. |
| `401` | Missing or invalid API key. |
| `403` | Your plan does not include API access. |
| `404` | No such broadcast on your account (or, on template selection, no such template). |
| `429` | Rate limited. Back off and retry. |
| `500` | Something went wrong on our side. Retry after a short wait. |

---

## Next steps

- [Broadcasts guide](../broadcasts/broadcasts.md) — the product behind these endpoints, including pacing and safety behaviour
- [Contacts API](contacts.md) — build the list a broadcast sends to
- [Templates API](templates.md) — manage the approved WhatsApp templates you can select from
- [Webhooks API](webhooks.md) — subscribe to `Broadcast Started` and `Broadcast Completed` instead of polling
