Skip to content

Sending & Scheduling

Send broadcasts immediately or schedule for later delivery

View .md

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:

ValidationError if Failed
Status is draft, approved, or scheduledbroadcast_already_sending
At least 1 contact existsno_contacts
Sender is configured for channelsender_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.

Code
Available Balance: $100.00 Broadcast Cost: $25.00 (estimated) ─────── Reserved Balance: $25.00 (blocked) Usable Balance: $75.00

How It Works

  1. Cost Estimation

    When you call /send, Zavu calculates the estimated cost based on contacts and channel rates

  2. Balance Check

    If your available balance (excluding other reservations) is less than the estimated cost, the request fails

  3. Reservation Created

    The estimated amount is reserved and cannot be used for other broadcasts or messages

  4. Messages Sent

    As messages are delivered, actual costs are deducted from the reservation

  5. Reservation Released

    When the broadcast completes or is cancelled, any unused reserved funds are released back to your available balance

Checking Your Balance

TypeScript
const 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

ScenarioWhat Happens
Broadcast completesUnused reservation released immediately
Broadcast cancelledFull reservation released (minus already-sent messages)
Actual cost < estimatedDifference released when broadcast completes
Actual cost > estimatedAdditional 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

  1. Submit for Sending

    When you call /send, the broadcast enters pending_review status

  2. AI Analysis

    Content is analyzed for spam, phishing, prohibited content, and policy violations

  3. Decision

    Broadcast is either approved and proceeds, or rejected with feedback

Review Statuses

StatusDescription
pending_reviewContent being analyzed
approvedReview passed, broadcast proceeds
rejectedContent rejected, needs editing
escalatedSent to human review
rejected_finalRejected 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:

RequirementExample
Include opt-out instructions"Reply STOP to unsubscribe"
Avoid spam trigger wordsAvoid "FREE", "ACT NOW", "LIMITED TIME" in all caps
Identify your businessInclude your company name
Don't impersonateNever pretend to be banks, government, etc.
Avoid URL shortenersUse 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 pending are marked skipped.
  • 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:

ChannelRateNotes
SMS10/secondShared across all projects
WhatsApp60/secondPer WhatsApp Business Account
Email14/second

For a 10,000 contact SMS broadcast:

  • Estimated time: ~17 minutes (10,000 / 10 per second)

Cost Estimation

Get estimated cost before sending:

TypeScript
const broadcast = await zavu.broadcasts.retrieve(broadcastId); console.log(`Estimated cost: $${broadcast.estimatedCost}`); console.log(`Total contacts: ${broadcast.totalContacts}`);
ChannelCost Model
SMSPer-message (varies by country)
WhatsAppFree (24h window) or template pricing
Email$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

TypeScript
import 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 Progress

Monitor delivery progress in real-time