# 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

```typescript
const broadcast = await zavu.broadcasts.send(broadcastId);

console.log(`Status: ${broadcast.status}`); // "pending_review"
```

```bash
curl -X POST https://api.zavu.dev/v1/broadcasts/{broadcastId}/send \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
```

### Response

```json
{
  "id": "brd_abc123",
  "name": "Weekly Newsletter",
  "status": "pending_review",
  "channel": "sms",
  "totalContacts": 1500,
  "reviewAttempts": 1,
  "createdAt": "2024-01-15T10:00:00.000Z"
}
```

> **Info:**
> All broadcasts go through an automated AI content review before sending. The broadcast enters `pending_review` status and, once approved, proceeds to `sending` automatically. Subscribe to the `broadcast.status_changed` webhook event to track the review process in real-time.

## Schedule for Later

Schedule a broadcast to send at a specific time:

```typescript
// 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}`);
```

```bash
curl -X POST https://api.zavu.dev/v1/broadcasts/{broadcastId}/send \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scheduledAt": "2024-01-16T09:00:00.000Z"
  }'
```

> **Tip:**
> Use scheduling to send campaigns at optimal times for your audience's timezone. For example, schedule a morning message to arrive at 9am in the recipient's local time.

## Rescheduling a Broadcast

If you need to change the scheduled time, you can reschedule a broadcast that's in `scheduled` status:

```typescript
const broadcast = await zavu.broadcasts.reschedule(broadcastId, {
  scheduledAt: "2024-01-17T14:00:00.000Z",
});

console.log(`New scheduled time: ${broadcast.scheduledAt}`);
```

```bash
curl -X PATCH https://api.zavu.dev/v1/broadcasts/{broadcastId}/schedule \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scheduledAt": "2024-01-17T14:00:00.000Z"
  }'
```

> **Note:**
> You can only reschedule broadcasts that are in `scheduled` status. The new time must be in the future.

## 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.

```
Available 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 rates
  
  
#### Balance 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

```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`.

> **Warning:**
> Reserved funds are blocked until the broadcast completes or is cancelled. Plan your campaigns to avoid blocking funds needed for other messaging.

### 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 |

> **Info:**
> WhatsApp message costs are billed directly by Meta, not deducted from your Zavu balance. No reservation is created for WhatsApp-only broadcasts.

## 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 enters `pending_review` status
  
  
#### AI Analysis

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

    Broadcast is either `approved` and proceeds, or `rejected` with 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:

```typescript
// 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"
}
```

```bash
# 1. Update the content
curl -X PATCH https://api.zavu.dev/v1/broadcasts/{broadcastId} \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Thanks for being a customer! Check out our latest products. Reply STOP to unsubscribe."
  }'

# 2. Retry the review
curl -X POST https://api.zavu.dev/v1/broadcasts/{broadcastId}/retry-review \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
```

> **Warning:**
> You have a maximum of 3 review attempts per broadcast. After 3 rejections, you must escalate to manual review or create a new broadcast.

### Escalating to Manual Review

If you believe your content was incorrectly rejected, escalate to the Zavu team:

```typescript
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"
```

```bash
curl -X POST https://api.zavu.dev/v1/broadcasts/{broadcastId}/escalate \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
```

> **Note:**
> Manual reviews are typically processed within 24 hours during business days. You'll receive a webhook notification when the review is complete.

### 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](https://www.zavu.dev/docs/guides/url-verification/overview)) |

## 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`:

```typescript
const broadcast = await zavu.broadcasts.cancel(broadcastId);

console.log(`Status: ${broadcast.status}`); // "cancelled"
```

```bash
curl -X POST https://api.zavu.dev/v1/broadcasts/{broadcastId}/cancel \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
```

### 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.

> **Warning:**
> Messages already queued or sent cannot be cancelled. Only pending contacts are skipped.

## Delivery Rate Limits

Zavu automatically rate-limits delivery to prevent carrier throttling:

| Channel | Rate | Notes |
|---------|------|-------|
| SMS | 10/second | Shared across all projects |
| WhatsApp | 60/second | Per WhatsApp Business Account |
| Email | 14/second | |

For a 10,000 contact SMS broadcast:
- **Estimated time**: ~17 minutes (10,000 / 10 per second)

> **Tip:**
> Large broadcasts are processed in the background. Use the [progress endpoint](https://www.zavu.dev/docs/guides/broadcasts/tracking-progress) to monitor delivery.

## 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}`);
```

| Channel | Cost Model |
|---------|------------|
| SMS | Per-message (varies by country) |
| WhatsApp | Free (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](https://www.zavu.dev/docs/guides/broadcasts/tracking-progress): Monitor delivery progress in real-time
