Broadcasts
One-off marketing campaigns to your contacts. Race-safe state machine with mid-send cancellation, send-time unsubscribe re-check, and live progress polling.
Broadcasts let you send a one-off email to all contacts (or contacts matching a tag) without setting up a recurring newsletter. They are managed via the dashboard, but the worker exposes a JWT-authenticated API for cancel and send operations.
State machine: draft → sending → sent | failed | cancelled
Production-hardening features:
- Race-safe transitions — every state-changing UPDATE includes the expected current status in the WHERE clause (e.g.
WHERE status='draft'), so a concurrent cancel that lands in the gap between page activations always wins. - Send-time unsubscribe re-check — before each individual SES handoff, the worker re-reads the contact's
unsubscribedfield. Contacts who unsubscribed after the initial page query but before queue insertion are skipped. - Audit log — every state change is appended to
broadcasts.audit_log(jsonb) with the action, timestamp, and details for debugging. - Cancel mid-send — the fan-out loop checks status before each page of contacts, stopping cleanly. Already-queued BullMQ jobs still deliver (we don't reach into the queue), but no further pages fan out.
- Live progress polling — the dashboard polls every 3 seconds while a broadcast is
sending, showing a livesent / totalcount and progress bar.
Throughput
Broadcast fan-out shares the same SES rate limit as everything else: AWS SES caps the account at 14 emails/sec and the Posthawk worker rate-limits dispatch to 12 emails/sec to leave headroom. So a broadcast to 1,000 contacts takes ~83 seconds to fully ship, 10,000 contacts ~14 minutes. The dashboard's live progress bar reflects the actual drain rate.
Two surfaces
- Public API (`/v1/broadcasts`, API-key) — create, list, get, send, and cancel broadcasts programmatically. Create + send require the
sendingscope; list + get requirereading. - Dashboard API (`/broadcasts`, JWT) — the same send/cancel operations plus schedule, used by the dashboard UI.
/v1/broadcastsCreate a broadcast. Requires the `sending` scope. Set recipient_type to "all" to send to every contact, or "tag" with a recipient_tag to target a segment.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Body
namestringrequiredInternal name for the broadcast
from_emailstringrequiredSender email — domain must be verified for sending
from_namestringoptionalDisplay name on the From header
subjectstringoptionalEmail subject line
html_bodystringoptionalHTML body
text_bodystringoptionalPlain text body
recipient_typestringoptional"all" (every contact) or "tag" (contacts matching recipient_tag)
recipient_tagstringoptionalTag to target when recipient_type is "tag"
curl -X POST https://api.posthawk.dev/v1/broadcasts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{
"name": "March Update",
"from_email": "hello@yourdomain.com",
"from_name": "Acme",
"subject": "What shipped in March",
"html_body": "<h1>This month at Acme</h1>...",
"recipient_type": "tag",
"recipient_tag": "newsletter"
}'{
"broadcast": {
"id": "broadcast-uuid",
"name": "March Update",
"status": "draft",
"from_email": "hello@yourdomain.com",
"recipient_type": "tag",
"recipient_tag": "newsletter"
}
}/v1/broadcastsList broadcasts in the workspace. Requires the `reading` scope. Supports status filter and pagination.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Query Parameters
statusstringoptionalFilter by status: draft, sending, sent, failed, cancelled
pagenumberoptionalPage number
curl https://api.posthawk.dev/v1/broadcasts \
-H "Authorization: Bearer your_api_key"/v1/broadcasts/:idGet a single broadcast by ID. Requires the `reading` scope.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Path Parameters
idstringrequiredBroadcast UUID
curl https://api.posthawk.dev/v1/broadcasts/{id} \
-H "Authorization: Bearer your_api_key"{
"broadcast": {
"id": "broadcast-uuid",
"name": "March Update",
"status": "sent",
"from_email": "hello@yourdomain.com"
}
}/v1/broadcasts/:id/sendSend a broadcast. Requires the `sending` scope. Race-safely transitions draft → sending and fans out to contacts. Returns when fan-out completes (or is cancelled mid-send).
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Path Parameters
idstringrequiredBroadcast UUID
curl -X POST https://api.posthawk.dev/v1/broadcasts/{id}/send \
-H "Authorization: Bearer your_api_key"{
"success": true,
"queued": 1247,
"message": "Broadcast sent to 1247 recipient(s)"
}/v1/broadcasts/:id/cancelCancel an in-flight or draft broadcast. Requires the `sending` scope. Already-queued BullMQ jobs still deliver, but no further pages of contacts are fanned out.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Path Parameters
idstringrequiredBroadcast UUID
curl -X POST https://api.posthawk.dev/v1/broadcasts/{id}/cancel \
-H "Authorization: Bearer your_api_key"{
"success": true,
"status": "cancelled",
"alreadyQueued": 423,
"message": "Cancelled — 423 already-queued email(s) will still be delivered"
}/broadcasts/:id/sendDashboard (JWT) equivalent of POST /v1/broadcasts/:id/send. Trigger a broadcast send. Race-safely transitions from draft → sending and starts fanning out contacts.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your JWT.
Path Parameters
idstringrequiredBroadcast UUID
curl -X POST https://api.posthawk.dev/broadcasts/{id}/send \
-H "Authorization: Bearer your_jwt_token"{
"success": true,
"queued": 1247,
"message": "Broadcast sent to 1247 recipient(s)"
}/broadcasts/:id/cancelDashboard (JWT) equivalent of POST /v1/broadcasts/:id/cancel. Cancel an in-flight or draft broadcast. Already-queued BullMQ jobs still deliver, but no further pages of contacts are fanned out.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your JWT.
Path Parameters
idstringrequiredBroadcast UUID
curl -X POST https://api.posthawk.dev/broadcasts/{id}/cancel \
-H "Authorization: Bearer your_jwt_token"{
"success": true,
"status": "cancelled",
"alreadyQueued": 423,
"message": "Cancelled — 423 already-queued email(s) will still be delivered"
}