About broadcast-campaign
broadcast-campaign is a skill for building Zavu campaigns that send messages to multiple recipients, including bulk email. It covers broadcast creation, contact management, review and scheduling, as well as progress monitoring and rejection handling.
Key features
- 01Create broadcasts across messaging channels
- 02Add contacts in batches with variables
- 03Submit, schedule, pause, or cancel campaigns
- 04Track delivery and recipient statuses
- 05Handle content-review rejection and retries
Use cases
- 01Send a bulk email newsletter
- 02Schedule a multi-recipient SMS campaign
- 03Monitor delivery and failed recipients
- 04Revise a broadcast rejected in content review
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ zavudev/ zavu-skills --skill broadcast-campaignAfter installing, type/broadcast-campaignin Claude Code to confirm it loaded.
Details
From the project READMEBroadcast Campaign
When to Use
Use this skill when building code to send messages to multiple recipients in a campaign — including bulk email. Covers the full broadcast lifecycle from creation to monitoring.
Broadcast Lifecycle
draft -> pending_review -> pending_admin_review -> approved -> sending -> completed
-> rejected -> (edit) -> pending_review (retry, max 3)
-> rejected -> escalated (manual review by Zavu team)
-> rejected_final
approved -> scheduled -> sending -> completed
sending -> paused (can resume)
draft | pending_review | pending_admin_review | approved
| rejected | escalated | scheduled | sending -> cancelled
(any) -> failed (permanent failure)
Step-by-Step Workflow
1. Create Broadcast
const result = await zavu.broadcasts.create({
name: "Black Friday Sale",
channel: "sms", // smart | sms | sms_oneway | whatsapp | telegram | email
text: "Hi {{name}}, check out our Black Friday deals! Code: FRIDAY20",
});
const broadcastId = result.broadcast.id; // brd_xxx
Python:
result = zavu.broadcasts.create(
name="Black Friday Sale",
channel="sms",
text="Hi {{name}}, check out our Black Friday deals! Code: FRIDAY20",
)
broadcast_id = result.broadcast.id
Go:
result, err := client.Broadcasts.Create(context.TODO(), zavudev.BroadcastCreateParams{
Name: zavudev.String("Black Friday Sale"),
Channel: zavudev.String("sms"),
Text: zavudev.String("Hi {{name}}, check out our Black Friday deals! Code: FRIDAY20"),
})
broadcastID := result.Broadcast.ID
Ruby:
result = client.broadcasts.create(
name: "Black Friday Sale",
channel: "sms",
text: "Hi {{name}}, check out our Black Friday deals! Code: FRIDAY20",
)
broadcast_id = result.broadcast.id
PHP:
$result = $client->broadcasts->create([
'name' => 'Black Friday Sale',
'channel' => 'sms',
'text' => 'Hi {{name}}, check out our Black Friday deals! Code: FRIDAY20',
]);
$broadcastId = $result->broadcast->id;
Channel Options
| Channel | Description |
|---|---|
smart | Per-contact intelligent routing |
sms | SMS to all contacts |
sms_oneway | One-way SMS (no replies) — needs no number and no credential |
whatsapp | WhatsApp (requires template for non-window contacts) |
telegram | Telegram. Recipients are numeric chat IDs (838360395 or telegram:838360395) of people who already wrote to your bot; a bot cannot start a chat, and phone numbers are rejected |
email | Email (needs emailSubject) — recommended path for bulk email |
Message Types
| Type | Description |
|---|---|
text | Plain text (default) |
image | Image with optional caption |
video | Video message |
audio | Audio message |
document | Document file |
template | WhatsApp pre-approved template |
Email Broadcast (with HTML body)
For bulk email campaigns, use channel: "email" with an HTML body. This is the recommended way to send mass email through Zavu.
Email broadcasts need a paid plan. On Free, sending or scheduling one returns 402 with code: "paid_plan_required" (drafts can still be created). A smart broadcast still sends, but its email recipients fail with errorCode: "PAID_PLAN_REQUIRED". Transactional email through POST /v1/messages works on every plan.
const result = await zavu.broadcasts.create({
name: "Newsletter",
channel: "email",
emailSubject: "Special offer for {{name}}",
text: "Hi {{name}}, check out our latest sale!", // plain text fallback
emailHtmlBody: "<h1>Hi {{name}}!</h1><p>Check out our latest sale.</p>",
metadata: { campaign_id: "camp_123", region: "US" },
});
2. Add Contacts (batch, max 1000/request)
const result = await zavu.broadcasts.contacts.add({
broadcastId: broadcastId,
contacts: [
{ recipient: "+14155551234", templateVariables: { name: "John" } },
{ recipient: "+14155555678", templateVariables: { name: "Jane" } },
],
});
console.log(result.added, result.duplicates, result.invalid);
3. Send (triggers content review)
Sending a broadcast needs no account verification, on any channel. Identity and business verification raise daily ceilings; neither is a permission to broadcast. What stands in front of a broadcast is the content review, and it cannot be bypassed.
The broadcast's own text is read once — not once per recipient — before the
automated review runs. A broadcast that is refused lands on rejected; one that
is flagged waits for a person even on email, which otherwise sends as soon as
the review passes it.
A whatsapp broadcast skips review. It can only be built on a template, and
Meta vets the business and the content when it approves that template. What is
enforced instead is that the template is approved — an unapproved one is refused
with 400 template_not_approved, and since WhatsApp passes no other review,
that is the only gate on it.
Each recipient is then sent as an ordinary message, so the daily ceilings and the 10DLC requirement on +1 SMS destinations apply per recipient.
The team owner gets an email when the review answers: approved (sending,
scheduled, or ready to send), or not approved, with the reason and what to do
next. A broadcast whose sender was deleted after it was created is refused with
400 sender_not_found before any balance is reserved; create a new broadcast
with another sender.
// Send immediately
await zavu.broadcasts.send({ broadcastId });
// Or schedule
await zavu.broadcasts.send({
broadcastId,
scheduledAt: "2024-01-15T10:00:00Z",
});
A draft never goes straight out — what happens next depends on the channel:
| Broadcast | What this call does |
|---|---|
| WhatsApp on a Meta-approved template | Skips review (Meta already vetted it) and starts sending |
| Automated review; sends as soon as it passes | |
| SMS, Telegram, smart, everything else | Automated review, then pending_admin_review — a person approves before it sends |
So a 202 means accepted, not sending. Poll the status rather than
assuming; only WhatsApp-template broadcasts move to sending immediately.
Calling send on a broadcast that is already approved or scheduled sends or
reschedules it directly, since it has already been through review.
When the balance cannot cover the estimated cost, send returns 402 with the
shared error shape at the top level: { code: "insufficient_balance", message, details: { totalBalance, reservedBalance, availableBalance } }. The same object
is also repeated under error for older integrations.
4. Monitor Progress
const progress = await zavu.broadcasts.progress({ broadcastId });
console.log(`${progress.percentComplete}% complete`);
console.log(`Delivered: ${progress.delivered}, Failed: ${progress.failed}, Skipped: ${progress.skipped}`);
console.log(`Estimated completion: ${progress.estimatedCompletionAt}`);
Per-contact statuses:
pending,queued,sending: not handed to the provider yet.sent: the provider accepted the message; delivery is not confirmed yet. A recipient moves on todeliveredorfailedwhen the channel reports it. Channels that never report delivery leave the recipient atsent.delivered: the channel confirmed delivery to the device (a WhatsApp read receipt counts too).failed: not delivered. A recipient can move fromsentordeliveredtofailedwhen the provider reports a failure late.skipped: not sent, because the recipient opted out of the channel or the broadcast was cancelled before reaching it.
The progress response and the broadcast carry the sent count as sent and sentCount, and sent is a valid status filter on GET /v1/broadcasts/{broadcastId}/contacts. The SDK does not type them yet, so read them from the REST response:
curl -s https://api.zavu.dev/v1/broadcasts/$BROADCAST_ID/progress \
-H "Authorization: Bearer $ZAVU_API_KEY"
# { "total": 500, "sent": 120, "delivered": 360, "failed": 20, ... }
Completion counts sent as done, so a completed broadcast can still have recipients waiting on a delivery receipt.
5. Handle Rejection (if content review fails)
// Check remaining review attempts
const broadcast = await zavu.broadcasts.get({ broadcastId });
console.log(`Review attempts: ${broadcast.reviewAttempts}/3`);
// Edit content
await zavu.broadcasts.update({
broadcastId,
text: "Updated message content with {{name}}",
});
// Retry review (max 3 attempts)
await zavu.broadcasts.retryReview({ broadcastId });
// Or escalate to manual review
await zavu.broadcasts.escalate({ broadcastId });
Other Operations
// Reschedule
await zavu.broadcasts.reschedule({
broadcastId, scheduledAt: "2024-01-16T14:00:00Z",
});
// Cancel (pending contacts skipped, queued may still deliver). Allowed from
// draft, pending_review, pending_admin_review, approved, rejected, escalated,
// scheduled and sending; not from paused or a finished status. Any reserved
// balance is released.
await zavu.broadcasts.cancel({ broadcastId });
// List contacts in broadcast
const contacts = await zavu.broadcasts.contacts.list({
broadcastId, status: "delivered", limit: 100,
});
// Delete (draft only)
await zavu.broadcasts.delete({ broadcastId });
Template Variables
Use {{variable_name}} in broadcast text. Override per-contact via templateVariables:
await zavu.broadcasts.contacts.add({
broadcastId,
contacts: [
{ recipient: "+14155551234", templateVariables: { name: "John", order_id: "ORD-001" } },
],
});
Constraints
- Max 1000 contacts per
addrequest (batch for larger lists) - Sending needs no account verification on any channel. What gates a broadcast is the content review below; drafting requires nothing
- Email broadcasts need a paid plan: on Free, send returns
402 paid_plan_required - Each recipient counts against the channel's daily ceiling (see the
send-messageskill); once it is reached the remaining recipients are markedfailedwitherrorCode: "DAILY_LIMIT_EXCEEDED"and are not retried the next day - Content goes through review before sending, except WhatsApp on a Meta-approved template
- Most channels also wait on a human (
pending_admin_review) after the automated pass; email is the exception and sends straight through, unless the review asks for a person - Balance is reserved (estimated cost) when sending
- Max 3 review retry attempts, then escalate
- Can only update/delete broadcasts in
draftstatus - Cancelling doesn't stop already-queued messages