Skip to content

Broadcasts ​

One email to every contact in a segment, personalised per recipient.

This is a separate path from POST /v1/send on purpose: a single send builds one message addressed to all its recipients, so it cannot carry a per-recipient body or unsubscribe link. A broadcast fans out into one message and one send job per recipient.

A broadcast of N recipients costs N messages of quota

That is correct, and it is the whole reason /v1/send was left alone.

The broadcast object ​

json
{
  "id": "…",
  "name": "March update",
  "segment_id": "…",
  "template_id": "…",
  "subject": "What shipped in March",
  "sender_email": "hello@example.com",
  "sender_alias": "Example",
  "status": "sent",
  "recipient_count": 1204,
  "queued_count": 1204,
  "skipped_count": 0,
  "last_error": null,
  "created_at": "…",
  "updated_at": "…",
  "started_at": "…",
  "finished_at": "…",
  "stats": { "total": 1204, "delivered": 1187, "failed": 9, "pending": 8 }
}

Two sets of numbers, and they mean different things ​

Describes
recipient_count, queued_count, skipped_countWhat the fan-out did
statsWhat became of the messages it wrote

recipient_count is the size of the audience when the send started; queued_count how many of them actually got a message; skipped_count the difference — normally an opt-out, or the monthly message quota running out partway.

stats.pending covers both queued and processing, so the three outcome buckets always add up to stats.total. Every broadcast carries stats, including a draft, where it is all zeros.

Create ​

POST /v1/broadcasts
bash
curl -X POST http://api.localhost/v1/broadcasts \
  -H "X-API-Key: $NOTIFYZR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "March update",
    "segment_id": "…",
    "template_id": "…",
    "subject": "What shipped in March",
    "sender_email": "hello@example.com",
    "sender_alias": "Example"
  }'
FieldRequiredNotes
nameyesUp to 200 characters.
segment_idyesThe audience.
template_idyesMust be published — a draft is refused.
subjectyes
sender_emailyesMust be on a verified domain.
sender_aliasnoDisplay name.

Created as a draft. Nothing is sent until you call /send.

Send ​

POST /v1/broadcasts/{id}/send

Returns as soon as the fan-out is queued. Nothing is mailed synchronously.

draft ──▶ queued ──▶ sending ──▶ sent
                              └▶ failed

There is no way back to draft, and there is deliberately no paused status — stopping midway leaves an audience half-mailed.

Things that will surprise you otherwise ​

The audience is resolved when the worker runs, not when you call /send. A segment is a live filter, and freezing 50,000 addresses into a queue message would be both enormous and already stale by the time it ran.

Suppressed addresses are excluded during fan-out, in the query that pages through the audience — so a mostly-opted-out segment does not produce runs of near-empty batches.

Quota is re-checked per recipient. A long fan-out can cross the ceiling partway through. That is not a failure: what was sent was sent, the rest are counted in skipped_count, and the broadcast completes.

A failed fan-out is not retried. Retrying after a partial fan-out would re-mail everyone it had already reached, so the reason is recorded in last_error for an operator instead.

Edit and delete are draft-only — 422 otherwise. Once queued, the content is partly out the door, and a sent broadcast's messages would be orphaned by a delete.

Reading the results ​

bash
curl http://api.localhost/v1/broadcasts/$ID -H "X-API-Key: $KEY"

# the individual messages
curl "http://api.localhost/v1/messages?broadcast_id=$ID" -H "X-API-Key: $KEY"

Emits broadcast.sent when the fan-out completes.

Released under the Apache 2.0 License.