> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zavu.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

<CodeGroup>
  ```typescript TypeScript theme={null}
  const broadcast = await zavu.broadcasts.send(broadcastId);

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

  ```python Python theme={null}
  broadcast = zavu.broadcasts.send(broadcast_id)

  print(f"Status: {broadcast.status}")  # "pending_review"
  ```

  ```ruby Ruby theme={null}
  broadcast = client.broadcasts.send(broadcast_id)

  puts "Status: #{broadcast.status}"  # "pending_review"
  ```

  ```go Go theme={null}
  broadcast, _ := client.Broadcasts.Send(context.TODO(), broadcastID, zavudev.BroadcastSendParams{})

  fmt.Printf("Status: %s\n", broadcast.Status)  // "pending_review"
  ```

  ```php PHP theme={null}
  $broadcast = $client->broadcasts->send($broadcastId);

  echo "Status: {$broadcast->status}\n";  // "pending_review"
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.zavu.dev/v1/broadcasts/{broadcastId}/send \
    -H "Authorization: Bearer $ZAVU_API_KEY"
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "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.
</Info>

## Schedule for Later

Schedule a broadcast to send at a specific time:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // 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}`);
  ```

  ```python Python theme={null}
  broadcast = zavu.broadcasts.send(
      broadcast_id,
      scheduled_at="2024-01-16T09:00:00.000Z"
  )

  print(f"Status: {broadcast.status}")  # "scheduled"
  print(f"Scheduled for: {broadcast.scheduled_at}")
  ```

  ```ruby Ruby theme={null}
  broadcast = client.broadcasts.send(broadcast_id,
    scheduled_at: "2024-01-16T09:00:00.000Z"
  )

  puts "Status: #{broadcast.status}"             # "scheduled"
  puts "Scheduled for: #{broadcast.scheduled_at}"
  ```

  ```go Go theme={null}
  broadcast, _ := client.Broadcasts.Send(context.TODO(), broadcastID, zavudev.BroadcastSendParams{
  	ScheduledAt: zavudev.String("2024-01-16T09:00:00.000Z"),
  })

  fmt.Printf("Status: %s\n", broadcast.Status)           // "scheduled"
  fmt.Printf("Scheduled for: %s\n", broadcast.ScheduledAt)
  ```

  ```php PHP theme={null}
  $broadcast = $client->broadcasts->send($broadcastId, [
      'scheduledAt' => '2024-01-16T09:00:00.000Z',
  ]);

  echo "Status: {$broadcast->status}\n";             // "scheduled"
  echo "Scheduled for: {$broadcast->scheduledAt}\n";
  ```

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

<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.
</Tip>

## Rescheduling a Broadcast

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

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

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

  ```python Python theme={null}
  broadcast = zavu.broadcasts.reschedule(
      broadcast_id,
      scheduled_at="2024-01-17T14:00:00.000Z"
  )

  print(f"New scheduled time: {broadcast.scheduled_at}")
  ```

  ```ruby Ruby theme={null}
  broadcast = client.broadcasts.reschedule(broadcast_id,
    scheduled_at: "2024-01-17T14:00:00.000Z"
  )

  puts "New scheduled time: #{broadcast.scheduled_at}"
  ```

  ```go Go theme={null}
  broadcast, _ := client.Broadcasts.Reschedule(context.TODO(), broadcastID, zavudev.BroadcastRescheduleParams{
  	ScheduledAt: zavudev.String("2024-01-17T14:00:00.000Z"),
  })

  fmt.Printf("New scheduled time: %s\n", broadcast.ScheduledAt)
  ```

  ```php PHP theme={null}
  $broadcast = $client->broadcasts->reschedule($broadcastId, [
      'scheduledAt' => '2024-01-17T14:00:00.000Z',
  ]);

  echo "New scheduled time: {$broadcast->scheduledAt}\n";
  ```

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

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

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

<Steps>
  <Step title="Cost Estimation">
    When you call `/send`, Zavu calculates the estimated cost based on contacts and channel rates
  </Step>

  <Step title="Balance Check">
    If your available balance (excluding other reservations) is less than the estimated cost, the request fails
  </Step>

  <Step title="Reservation Created">
    The estimated amount is reserved and cannot be used for other broadcasts or messages
  </Step>

  <Step title="Messages Sent">
    As messages are delivered, actual costs are deducted from the reservation
  </Step>

  <Step title="Reservation Released">
    When the broadcast completes or is cancelled, any unused reserved funds are released back to your available balance
  </Step>
</Steps>

### Checking Your Balance

```typescript theme={null}
const billing = await zavu.billing.get();

console.log(`Total Balance: $${billing.balance}`);
console.log(`Reserved: $${billing.reservedBalance}`);
console.log(`Available: $${billing.availableBalance}`);
```

### Response with Reservation

When a broadcast starts, the response includes reservation details:

```json theme={null}
{
  "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:

```json theme={null}
{
  "code": "insufficient_balance",
  "message": "Insufficient balance for this broadcast",
  "details": {
    "estimatedCost": 25.00,
    "availableBalance": 10.50,
    "reservedBalance": 15.00,
    "totalBalance": 25.50,
    "requiredAmount": 14.50
  }
}
```

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

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

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

<Steps>
  <Step title="Submit for Sending">
    When you call `/send`, the broadcast enters `pending_review` status
  </Step>

  <Step title="AI Analysis">
    Content is analyzed for spam, phishing, prohibited content, and policy violations
  </Step>

  <Step title="Decision">
    Broadcast is either `approved` and proceeds, or `rejected` with feedback
  </Step>
</Steps>

### 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 theme={null}
{
  "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:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // 1. Get the rejected broadcast
  const broadcast = await zavu.broadcasts.get(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"
  }
  ```

  ```python Python theme={null}
  # 1. Get the rejected broadcast
  broadcast = zavu.broadcasts.get(broadcast_id)

  if broadcast.status == "rejected":
      print(f"Rejection reason: {broadcast.review_result.reasoning}")

      # 2. Edit the content to address issues
      zavu.broadcasts.update(broadcast_id,
          text="Thanks for being a customer! Check out our latest products. Reply STOP to unsubscribe."
      )

      # 3. Retry the review
      updated = zavu.broadcasts.retry_review(broadcast_id)
      print(f"New status: {updated.status}")  # "pending_review"
  ```

  ```ruby Ruby theme={null}
  # 1. Get the rejected broadcast
  broadcast = client.broadcasts.get(broadcast_id)

  if broadcast.status == "rejected"
    puts "Rejection reason: #{broadcast.review_result&.reasoning}"

    # 2. Edit the content to address issues
    client.broadcasts.update(broadcast_id,
      text: "Thanks for being a customer! Check out our latest products. Reply STOP to unsubscribe."
    )

    # 3. Retry the review
    updated = client.broadcasts.retry_review(broadcast_id)
    puts "New status: #{updated.status}"  # "pending_review"
  end
  ```

  ```go Go theme={null}
  // 1. Get the rejected broadcast
  broadcast, _ := client.Broadcasts.Get(context.TODO(), broadcastID)

  if broadcast.Status == "rejected" {
  	fmt.Printf("Rejection reason: %s\n", broadcast.ReviewResult.Reasoning)

  	// 2. Edit the content to address issues
  	client.Broadcasts.Update(context.TODO(), broadcastID, zavudev.BroadcastUpdateParams{
  		Text: zavudev.String("Thanks for being a customer! Check out our latest products. Reply STOP to unsubscribe."),
  	})

  	// 3. Retry the review
  	updated, _ := client.Broadcasts.RetryReview(context.TODO(), broadcastID)
  	fmt.Printf("New status: %s\n", updated.Status) // "pending_review"
  }
  ```

  ```php PHP theme={null}
  // 1. Get the rejected broadcast
  $broadcast = $client->broadcasts->get($broadcastId);

  if ($broadcast->status === 'rejected') {
      echo "Rejection reason: {$broadcast->reviewResult->reasoning}\n";

      // 2. Edit the content to address issues
      $client->broadcasts->update($broadcastId, [
          'text' => 'Thanks for being a customer! Check out our latest products. Reply STOP to unsubscribe.',
      ]);

      // 3. Retry the review
      $updated = $client->broadcasts->retryReview($broadcastId);
      echo "New status: {$updated->status}\n"; // "pending_review"
  }
  ```

  ```bash cURL theme={null}
  # 1. Update the content
  curl -X PATCH https://api.zavu.dev/v1/broadcasts/{broadcastId} \
    -H "Authorization: Bearer $ZAVU_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 $ZAVU_API_KEY"
  ```
</CodeGroup>

<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.
</Warning>

### Escalating to Manual Review

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

<CodeGroup>
  ```typescript TypeScript theme={null}
  const broadcast = await zavu.broadcasts.escalate(broadcastId);

  console.log("Status:", broadcast.status); // "escalated"
  // Zavu team will review and update status to "approved" or "rejected_final"
  ```

  ```python Python theme={null}
  broadcast = zavu.broadcasts.escalate(broadcast_id)

  print(f"Status: {broadcast.status}")  # "escalated"
  ```

  ```ruby Ruby theme={null}
  broadcast = client.broadcasts.escalate(broadcast_id)

  puts "Status: #{broadcast.status}"  # "escalated"
  ```

  ```go Go theme={null}
  broadcast, _ := client.Broadcasts.Escalate(context.TODO(), broadcastID)

  fmt.Printf("Status: %s\n", broadcast.Status) // "escalated"
  ```

  ```php PHP theme={null}
  $broadcast = $client->broadcasts->escalate($broadcastId);

  echo "Status: {$broadcast->status}\n"; // "escalated"
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.zavu.dev/v1/broadcasts/{broadcastId}/escalate \
    -H "Authorization: Bearer $ZAVU_API_KEY"
  ```
</CodeGroup>

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

### 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](/guides/url-verification/overview)) |

## Cancelling a Broadcast

Cancel a broadcast that hasn't completed:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const broadcast = await zavu.broadcasts.cancel(broadcastId);

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

  ```python Python theme={null}
  broadcast = zavu.broadcasts.cancel(broadcast_id)

  print(f"Status: {broadcast.status}")  # "cancelled"
  ```

  ```ruby Ruby theme={null}
  broadcast = client.broadcasts.cancel(broadcast_id)

  puts "Status: #{broadcast.status}"  # "cancelled"
  ```

  ```go Go theme={null}
  broadcast, _ := client.Broadcasts.Cancel(context.TODO(), broadcastID)

  fmt.Printf("Status: %s\n", broadcast.Status) // "cancelled"
  ```

  ```php PHP theme={null}
  $broadcast = $client->broadcasts->cancel($broadcastId);

  echo "Status: {$broadcast->status}\n"; // "cancelled"
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.zavu.dev/v1/broadcasts/{broadcastId}/cancel \
    -H "Authorization: Bearer $ZAVU_API_KEY"
  ```
</CodeGroup>

### What Happens on Cancel

| Original Status | Action                                                   |
| --------------- | -------------------------------------------------------- |
| `draft`         | Broadcast deleted                                        |
| `scheduled`     | Returns to `draft`, can be rescheduled                   |
| `sending`       | Stops new messages, pending contacts marked as `skipped` |

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

## 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 | Via AWS SES                   |

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](/guides/broadcasts/tracking-progress) to monitor delivery.
</Tip>

## Cost Estimation

Get estimated cost before sending:

```typescript theme={null}
const broadcast = await zavu.broadcasts.get(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                    |

Plus MAU (Monthly Active User) charge of \$0.10 per unique recipient per month.

## Complete Workflow Example

```typescript theme={null}
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.addContacts(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.get(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.getProgress(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

<Card title="Tracking Progress" icon="chart-simple" href="/guides/broadcasts/tracking-progress">
  Monitor delivery progress in real-time
</Card>
