> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lazyapp.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Broadcasts

> Template sends to many contacts, with compliance built in.

A broadcast sends one approved **marketing** template to a saved segment or an explicit list of phone numbers. LazyApp creates the campaign, resolves the audience, paces sends to your number's Meta messaging tier, and tracks delivery through to read receipts.

> [!NOTE]
>
> Broadcasts in live mode count against your plan's monthly sends: 2,000 on Starter, 15,000 on Growth and 1,00,000 (fair use) on Pro. Test mode is never limited. See [Monthly broadcast sends](#monthly-broadcast-sends) and [Plans and limits](https://docs.lazyapp.in/billing/pricing-and-tiers.md#feature-gating).

## Quick start

LazyApp uses **one create call** with recipients inline — you can still [add more recipients](#add-recipients) to a draft or scheduled broadcast later.

```bash Create and start now theme={null}
curl https://lazyapp.in/v1/broadcasts \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "August sale",
    "template_name": "seasonal_offer",
    "language": "en",
    "phone_numbers": ["+919812345670", "+919812345671"],
    "variable_map": {
      "body": ["20% off", "Sunday"]
    },
    "start": true
  }'
```

```bash Create a draft, start later theme={null}
curl https://lazyapp.in/v1/broadcasts \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "August sale",
    "template_name": "seasonal_offer",
    "language": "en",
    "contact_segment_uuid": "11111111-1111-1111-1111-111111111111",
    "start": false
  }'

curl -X POST https://lazyapp.in/v1/broadcasts/{uuid}/start \
  -H "Authorization: Bearer $LAZYAPP_API_KEY"
```

The create response includes `uuid`, `status`, and `stats`. Poll [`GET /v1/broadcasts/{uuid}`](https://docs.lazyapp.in/api-reference/broadcasts/retrieve-a-broadcast) while `status` is `running`, or subscribe to [`campaign.*`](https://docs.lazyapp.in/webhooks/events#campaigns) webhooks.

## Create fields

| Field | Required | Notes |
| - | - | - |
| `name` | Yes | Label in the console and webhooks |
| `template_name` | Yes | Must be an **approved marketing** template in this workspace |
| `language` | Yes | Template language code (for example `en` or `en_US`) |
| `phone_numbers` | One of these | Up to **50,000** E.164 numbers |
| `contact_segment_uuid` | One of these | Prefer this public id from [`GET /v1/contact-segments`](https://docs.lazyapp.in/guides/contacts#segments) |
| `contact_segment_id` | One of these | Integer id (legacy; prefer uuid) |
| `variable_map` | No | Static or per-recipient template parameters — see [Template variables](#template-variables) |
| `schedule_at` | No | ISO 8601 datetime. Status becomes `scheduled` when set without `start: true` |
| `quiet_hours` | No | `{ "start": "21:00", "end": "09:00", "timezone": "Asia/Kolkata" }` |
| `frequency_cap_days` | No | Skip contacts who received marketing inside this window (1–365 days) |
| `whatsapp_connection_id` | No | Internal connection id when the workspace has multiple numbers |
| `start` | No | When `true`, starts immediately after create |

There is no `profileId`, `accountId`, or `platform` field — the API key already scopes the request to one workspace and mode (`live` / `test`).

## Template variables

Pass static placeholder values, or personalise per contact:

```json theme={null}
{
  "variable_map": {
    "header": "Summer sale",
    "body": [
      { "source": "contact_attribute", "key": "first_name" },
      { "source": "static", "value": "20% off" },
      { "source": "contact_field", "field": "phone_number" }
    ]
  }
}
```

* String / `string[]` shorthand still means **the same values for every recipient**.
* Object slots use `source`: `static`, `contact_attribute` (`key`), or `contact_field` (`field` such as `phone_number`, `email`, `username`).
* Missing attributes become an empty string so Meta still gets the expected parameter count.
* `header` fills a text header variable when the template has one.
* `header_media` is the image, video or document for a media header: a public https URL or an uploaded media id. Carousel cards take theirs in `features.cards[].header_media`.
* `body` is an ordered array matching `{{1}}`, `{{2}}`, … in the template body.

The number of `body` entries must match the template's placeholder count. A mismatch makes Meta reject the send (parameter-count error).

An image-header marketing template, such as an intake announcement:

```json theme={null}
{
  "variable_map": {
    "header_media": "https://cdn.acme.example/campus.jpg",
    "body": [{ "source": "contact_attribute", "key": "first_name" }, "Autumn 2027"]
  }
}
```

The shape of `variable_map` is checked when you create the broadcast, so a misspelt `source` or a `header_media` that is not a string is a `422` now rather than a failed send for every recipient later.

## Add recipients

Only `draft` or `scheduled` broadcasts. Existing recipient rows are kept; new numbers are merged and compliance-checked.

```bash theme={null}
curl -X POST https://lazyapp.in/v1/broadcasts/{uuid}/recipients \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_numbers": ["+919812345672"],
    "recipients": [
      {
        "phone_number": "+919812345673",
        "variable_map": { "body": ["Asha"] }
      }
    ]
  }'
```

## Recipients

Target either:

1. **`phone_numbers`** — explicit E.164 list (`+919812345670`).
2. **`contact_segment_uuid`** — a [segment](https://docs.lazyapp.in/guides/contacts.md#segments) you created over the API or in the console. `contact_segment_id` (the integer id) also works.

Recipients must already exist as **contacts** in the workspace with **granted marketing consent**. Numbers that are not contacts, or contacts without consent, are counted in `stats.excluded` with a reason — they are never sent.

Workflow for a cold list:

```bash theme={null}
# 1. Create the contact
curl https://lazyapp.in/v1/contacts \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone_number": "+919812345670", "name": "Asha"}'

# 2. Record marketing consent from your opt-in flow
curl https://lazyapp.in/v1/contacts/{contact}/consent \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "granted": true,
    "purpose": "marketing",
    "consent_text": "I agree to receive promotional messages on WhatsApp",
    "proof_reference": "form-submission-1042",
    "captured_by": "website-checkout"
  }'

# 3. Include the number in the broadcast
```

To target everyone with a tag, create a [segment](https://docs.lazyapp.in/guides/contacts.md#segments) filtering on the tag name and pass its `uuid` as `contact_segment_uuid`.

## Scheduling

Set `schedule_at` on create and omit `start` (or pass `"start": false`):

```json theme={null}
{
  "name": "Diwali blast",
  "template_name": "festive_offer",
  "language": "en",
  "phone_numbers": ["+919812345670"],
  "schedule_at": "2026-11-01T10:00:00+05:30",
  "start": false
}
```

The broadcast stays `scheduled` until LazyApp's scheduler starts it at `schedule_at`, or you call [`POST /v1/broadcasts/{uuid}/start`](https://docs.lazyapp.in/api-reference/broadcasts/start-a-broadcast) early.

There is no separate `POST …/schedule` endpoint — scheduling is a field on create.

## Compliance is enforced at send time, not create time

This is deliberate. A broadcast created on Monday and sent on Friday respects the opt-outs that happened on Wednesday.

> **Opt-outs**
>
> Suppressed contacts are excluded. There is no override.

> **Frequency caps**
>
> `frequency_cap_days` skips anyone who already received a marketing message inside that
> window.

> **Quiet hours**
>
> Sending pauses during quiet hours in the contact's timezone and resumes after.

> **Messaging limits**
>
> Sends are paced to your number's Meta messaging tier rather than being rejected in bulk.

### Exclusion reasons

When you start a broadcast, `stats` breaks down the audience:

| `stats` field | Meaning |
| - | - |
| `audience` | Total rows (eligible + excluded) |
| `excluded` | Skipped before send |
| `pending` | Waiting to send |
| `sent` | Handed to Meta |
| `delivered` | Delivery confirmed |
| `read` | Read receipt received |
| `failed` | Send or delivery failed |

Excluded recipients are stored with a reason code (visible in the console). Common codes:

| Reason | Cause |
| - | - |
| `unknown_contact` | Phone is not a contact in this workspace |
| `no_marketing_consent` | No granted marketing consent on file |
| `suppressed` | Contact opted out of marketing |
| `frequency_cap` | Already received marketing inside `frequency_cap_days` |
| `quiet_hours` | Send time falls in configured quiet hours |
| `country_excluded` | Country filter on the campaign (console) |
| `marketing_limit` | Per-contact daily marketing cap reached |

## Recording consent from your own opt-in flow

Broadcasts refuse recipients without granted marketing consent. Consent collected on your own website — a checkbox, a form, a CRM export — can be recorded with:

```bash theme={null}
curl https://lazyapp.in/v1/contacts/{contact}/consent \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "granted": true,
    "purpose": "marketing",
    "consent_text": "I agree to receive promotional messages on WhatsApp",
    "proof_reference": "form-submission-1042",
    "captured_by": "website-checkout"
  }'
```

Consent is evidence, not just a flag — `consent_text`, `proof_reference` and `captured_by` are stored exactly as given. Granting marketing consent also lifts a matching suppression, so someone who just opted in on your site is not still opted out.

## Monthly broadcast sends

Your plan sets how many live-mode broadcast recipients you can send to in a calendar month (UTC):

| Plan | Broadcast sends a month |
| - | - |
| Starter | 2,000 |
| Growth | 15,000 |
| Pro | 1,00,000 (fair use) |

Every recipient that is queued or sent counts, across all broadcasts in the workspace. The count resets on the 1st of the month. Test mode is never limited, so you can rehearse a broadcast on the test number as often as you like.

* **Starting checks the whole audience.** If a broadcast has more pending recipients than you have sends left this month, LazyApp refuses to start it, and the message says how many sends remain and when they reset. Send to fewer people or move to a bigger plan in **Billing**. Over the API, the start call returns `422` with that reason in `message`.
* **A scheduled broadcast that no longer fits fails.** It moves to `failed` with the reason, rather than retrying every minute.
* **A running broadcast pauses when the month is used up.** Other broadcasts can use sends after this one started, so LazyApp checks again before each batch. When nothing is left, the broadcast moves to `paused` and your billing contacts get an email. Start it again next month, or after you upgrade, from the console or with `POST /v1/broadcasts/{uuid}/start`.

### What a broadcast costs

The send limit is what your plan includes. The messages themselves are billed by Meta, at Meta's rates, to your own Meta payment method. LazyApp adds 0%. See [How Meta bills your messages](https://docs.lazyapp.in/billing/meta-message-billing.md) and [WhatsApp rates](https://docs.lazyapp.in/billing/whatsapp-rates.md).

## Status and webhooks

| Status | Meaning |
| - | - |
| `draft` | Created, not scheduled or started |
| `scheduled` | Waiting for `schedule_at` |
| `running` | Sends in progress |
| `completed` | All recipients processed |
| `failed` | Stopped with an error (for example template missing) |
| `paused` | Paused from the console, or because the month's [broadcast sends](#monthly-broadcast-sends) ran out. Start it again to resume |
| `stopped` | Stopped from the console |

Webhook events:

| Event | When |
| - | - |
| `campaign.started` | Sending began |
| `campaign.completed` | All batches finished |
| `campaign.failed` | Campaign stopped early |

Per-message delivery still arrives on the usual [`message.*`](https://docs.lazyapp.in/webhooks/events#messages) events for each recipient.

> [!WARNING]
>
> Marketing templates need an opt-in you can evidence. Broadcasting to a purchased list is
> the fastest route to having your number's quality rating cut and your messaging limit
> reduced.

## What we skip (Zernio-style)

| Zernio | LazyApp |
| - | - |
| Create → add recipients → send (three calls) | One `POST /v1/broadcasts` with `phone_numbers` or `contact_segment_uuid` |
| `profileId` + `accountId` + `platform: whatsapp` | Workspace scoped by API key |
| `variableMapping` from contact fields | `variable_map` slots with `contact_attribute` or `contact_field` |
| `POST …/recipients` with `contactIds` or `useSegment` | Recipients on create; segments by id |
| `POST …/schedule` | `schedule_at` on create |
| Utility/auth templates in broadcasts | **Marketing** templates only |

## Related

* [Templates](https://docs.lazyapp.in/sending/templates.md) — create and approve marketing templates
* [Messages](https://docs.lazyapp.in/sending/messages.md) — one-off template sends with per-recipient control
* [Webhooks](https://docs.lazyapp.in/webhooks/events.md#campaigns) — `campaign.*` events
* [Plans and limits](https://docs.lazyapp.in/billing/pricing-and-tiers.md#feature-gating) — monthly broadcast sends per plan
* [How Meta bills your messages](https://docs.lazyapp.in/billing/meta-message-billing.md) — Meta's rates, 0% added by LazyApp


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.