Sending & Scheduling
Send broadcasts immediately or schedule for later delivery
Once you've added contacts to your broadcast, trigger the send to start delivering messages.
Send Immediately
const broadcast = await zavu.broadcasts.send(broadcastId);
console.log(`Status: ${broadcast.status}`); // "pending_review"
Response
JSON{ "id": "brd_abc123", "name": "Weekly Newsletter", "status": "pending_review", "channel": "sms", "totalContacts": 1500, "reviewAttempts": 1, "createdAt": "2024-01-15T10:00:00.000Z" }
Schedule for Later
Schedule a broadcast to send at a specific time:
// Schedule for tomorrow at 9am UTC
const broadcast = await zavu.broadcasts.send(broadcastId, {
scheduledAt: "2024-01-16T09:00:00.000Z",
});
console.log(`Status: ${broadcast.status}`); // "scheduled"
console.log(`Scheduled for: ${broadcast.scheduledAt}`);
Rescheduling a Broadcast
If you need to change the scheduled time, you can reschedule a broadcast that's in scheduled status:
const broadcast = await zavu.broadcasts.reschedule(broadcastId, {
scheduledAt: "2024-01-17T14:00:00.000Z",
});
console.log(`New scheduled time: ${broadcast.scheduledAt}`);
Pre-Send Validation
Before sending, the API validates:
| Validation | Error if Failed |
|---|---|
Status is draft, approved, or scheduled | broadcast_already_sending |
| At least 1 contact exists | no_contacts |
| Sender is configured for channel | sender_not_configured |
| Sufficient balance (SMS/Email) | insufficient_balance |
| Template exists and approved (WhatsApp) | template_not_approved |
Balance Reservation
When you trigger a broadcast, Zavu reserves the estimated cost from your balance. This ensures funds are available for the entire campaign.
CodeAvailable Balance: $100.00 Broadcast Cost: $25.00 (estimated) ─────── Reserved Balance: $25.00 (blocked) Usable Balance: $75.00
How It Works
Cost Estimation
When you call
/send, Zavu calculates the estimated cost based on contacts and channel ratesBalance Check
If your available balance (excluding other reservations) is less than the estimated cost, the request fails
Reservation Created
The estimated amount is reserved and cannot be used for other broadcasts or messages
Messages Sent
As messages are delivered, actual costs are deducted from the reservation
Reservation Released
When the broadcast completes or is cancelled, any unused reserved funds are released back to your available balance
Checking Your Balance
TypeScriptconst billing = await zavu.balance.retrieve(); // `balance` is in cents. console.log(`Balance: $${(billing.balance / 100).toFixed(2)}`);
Reserved and available amounts are not part of the balance response. When a
send is refused for funds, the 402 body carries them under details:
JSON{ "code": "insufficient_balance", "message": "Insufficient balance. Available: $50.00", "details": { "totalBalance": 50, "reservedBalance": 25, "availableBalance": 25 } }
Response with Reservation
When a broadcast starts, the response includes reservation details:
JSON{ "id": "brd_abc123", "status": "sending", "totalContacts": 1500, "estimatedCost": 25.00, "reservedAmount": 25.00, "startedAt": "2024-01-15T10:30:00.000Z" }
Insufficient Balance
If you don't have enough available balance, the send returns 402:
JSON{ "code": "insufficient_balance", "message": "Insufficient balance. Available: $10.50", "details": { "totalBalance": 25.50, "reservedBalance": 15.00, "availableBalance": 10.50 } }
The same object is also repeated under an error key, for integrations written against the older shape. Read the top-level code.
Reservation Release
| Scenario | What Happens |
|---|---|
| Broadcast completes | Unused reservation released immediately |
| Broadcast cancelled | Full reservation released (minus already-sent messages) |
| Actual cost < estimated | Difference released when broadcast completes |
| Actual cost > estimated | Additional amount charged from available balance |
Content Review
All broadcasts go through an automated AI content review before sending. This protects both you and your recipients from policy violations.
How Content Review Works
Submit for Sending
When you call
/send, the broadcast enterspending_reviewstatusAI Analysis
Content is analyzed for spam, phishing, prohibited content, and policy violations
Decision
Broadcast is either
approvedand proceeds, orrejectedwith feedback
Review Statuses
| Status | Description |
|---|---|
pending_review | Content being analyzed |
approved | Review passed, broadcast proceeds |
rejected | Content rejected, needs editing |
escalated | Sent to human review |
rejected_final | Rejected by human review (cannot be appealed) |
Handling Rejections
If your broadcast is rejected, you'll receive details about the issue:
JSON{ "id": "brd_abc123", "status": "rejected", "reviewResult": { "score": 0.35, "categories": ["spam_indicators", "missing_opt_out"], "reasoning": "Message contains promotional content without unsubscribe option", "flaggedContent": ["limited time offer", "act now"], "reviewedAt": "2024-01-15T10:31:00.000Z" }, "reviewAttempts": 1 }
Editing and Retrying
After rejection, edit your broadcast content and retry the review:
// 1. Get the rejected broadcast
const broadcast = await zavu.broadcasts.retrieve(broadcastId);
if (broadcast.status === "rejected") {
console.log("Rejection reason:", broadcast.reviewResult?.reasoning);
// 2. Edit the content to address issues
await zavu.broadcasts.update(broadcastId, {
text: "Thanks for being a customer! Check out our latest products. Reply STOP to unsubscribe.",
});
// 3. Retry the review
const updated = await zavu.broadcasts.retryReview(broadcastId);
console.log("New status:", updated.status); // "pending_review"
}
Escalating to Manual Review
If you believe your content was incorrectly rejected, escalate to the Zavu team:
const broadcast = await zavu.broadcasts.escalateReview(broadcastId);
console.log("Status:", broadcast.status); // "escalated"
// Zavu team will review and update status to "approved" or "rejected_final"
Content Guidelines
To avoid rejections, ensure your broadcasts:
| Requirement | Example |
|---|---|
| Include opt-out instructions | "Reply STOP to unsubscribe" |
| Avoid spam trigger words | Avoid "FREE", "ACT NOW", "LIMITED TIME" in all caps |
| Identify your business | Include your company name |
| Don't impersonate | Never pretend to be banks, government, etc. |
| Avoid URL shorteners | Use full URLs (see URL Verification) |
Cancelling a Broadcast
Cancel a broadcast from draft, pending_review, pending_admin_review, approved, rejected, escalated, scheduled or sending. A broadcast that is paused, rejected_final or already finished (completed, cancelled, failed) returns 400:
const broadcast = await zavu.broadcasts.cancel(broadcastId);
console.log(`Status: ${broadcast.status}`); // "cancelled"
What Happens on Cancel
Whatever status it was in, the broadcast moves to cancelled and stays there:
- Contacts still
pendingare markedskipped. - Any balance reserved for it is released.
- A content review still running when it is cancelled cannot bring it back.
Delivery Rate Limits
Zavu automatically rate-limits delivery to prevent carrier throttling:
| Channel | Rate | Notes |
|---|---|---|
| SMS | 10/second | Shared across all projects |
| 60/second | Per WhatsApp Business Account | |
| 14/second |
For a 10,000 contact SMS broadcast:
- Estimated time: ~17 minutes (10,000 / 10 per second)
Cost Estimation
Get estimated cost before sending:
TypeScriptconst broadcast = await zavu.broadcasts.retrieve(broadcastId); console.log(`Estimated cost: $${broadcast.estimatedCost}`); console.log(`Total contacts: ${broadcast.totalContacts}`);
| Channel | Cost Model |
|---|---|
| SMS | Per-message (varies by country) |
| Free (24h window) or template pricing | |
| $0.02 per message |
Messages also count toward your plan's monthly message limits, with usage-based overage applied beyond them. See your plan for the exact limits and rates.
Complete Workflow Example
TypeScriptimport Zavudev from '@zavudev/sdk'; const zavu = new Zavudev({ apiKey: process.env['ZAVUDEV_API_KEY'], // This is the default and can be omitted }); async function sendPromotion(contacts: Array<{phone: string, name: string}>) { // 1. Create broadcast const broadcast = await zavu.broadcasts.create({ name: "Holiday Sale 2024", channel: "sms", text: "Hi {{name}}! Our holiday sale is live. Get 30% off with code HOLIDAY30. Shop now!", }); console.log(`Created broadcast: ${broadcast.id}`); // 2. Add contacts in batches const BATCH_SIZE = 1000; for (let i = 0; i < contacts.length; i += BATCH_SIZE) { const batch = contacts.slice(i, i + BATCH_SIZE); const result = await zavu.broadcasts.contacts.add(broadcast.id, { contacts: batch.map(c => ({ recipient: c.phone, templateVariables: { name: c.name }, })), }); console.log(`Batch ${Math.floor(i / BATCH_SIZE) + 1}: Added ${result.added} contacts`); } // 3. Check estimated cost const updated = await zavu.broadcasts.retrieve(broadcast.id); console.log(`Estimated cost: $${updated.estimatedCost}`); console.log(`Total contacts: ${updated.totalContacts}`); // 4. Send the broadcast await zavu.broadcasts.send(broadcast.id); console.log("Broadcast started!"); // 5. Poll for progress let complete = false; while (!complete) { await new Promise(r => setTimeout(r, 5000)); // Wait 5 seconds const progress = await zavu.broadcasts.progress(broadcast.id); console.log(`Progress: ${progress.percentComplete}% (${progress.delivered} delivered, ${progress.failed} failed)`); if (progress.status === "completed" || progress.status === "cancelled") { complete = true; } } console.log("Broadcast complete!"); }
Next Steps
Tracking ProgressMonitor delivery progress in real-time