# Tracking Progress

> Monitor broadcast delivery in real-time

Track your broadcast's delivery progress with instant updates on sent, delivered, and failed messages.

## Get Progress

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

console.log(`Status: ${progress.status}`);
console.log(`Progress: ${progress.percentComplete}%`);
console.log(`Delivered: ${progress.delivered}/${progress.total}`);
console.log(`Failed: ${progress.failed}`);
```

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

### Response

```json
{
  "broadcastId": "brd_abc123",
  "status": "sending",
  "total": 5000,
  "pending": 2500,
  "sending": 100,
  "sent": 350,
  "delivered": 2000,
  "failed": 50,
  "skipped": 0,
  "percentComplete": 48.0,
  "estimatedCost": 75.00,
  "reservedAmount": 75.00,
  "actualCost": 35.25,
  "startedAt": "2024-01-15T10:30:00.000Z",
  "estimatedCompletionAt": "2024-01-15T10:45:00.000Z"
}
```

## Progress Fields

| Field | Type | Description |
|-------|------|-------------|
| `broadcastId` | string | Broadcast identifier |
| `status` | string | Current broadcast status |
| `total` | number | Total contacts in broadcast |
| `pending` | number | Not yet queued for sending |
| `sending` | number | Currently being sent |
| `sent` | number | Accepted by the provider, delivery not confirmed yet |
| `delivered` | number | Delivery to the device confirmed |
| `failed` | number | Failed to deliver |
| `skipped` | number | Skipped (broadcast cancelled) |
| `percentComplete` | number | Percentage complete (0-100) |
| `estimatedCost` | number | Estimated total cost (USD) |
| `reservedAmount` | number | Amount reserved from balance (USD) |
| `actualCost` | number | Actual cost so far (USD) |
| `startedAt` | string | When sending started |
| `estimatedCompletionAt` | string | Estimated completion time |

## Polling for Updates

Poll the progress endpoint at regular intervals:

```typescript
async function waitForCompletion(broadcastId: string) {
  const POLL_INTERVAL = 5000; // 5 seconds

  while (true) {
    const progress = await zavu.broadcasts.progress(broadcastId);

    console.log(`[${new Date().toISOString()}] ${progress.percentComplete}% complete`);
    console.log(`  Delivered: ${progress.delivered}`);
    console.log(`  Failed: ${progress.failed}`);
    console.log(`  Pending: ${progress.pending}`);

    if (progress.status === "completed" || progress.status === "cancelled") {
      return progress;
    }

    await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
  }
}

const finalProgress = await waitForCompletion(broadcastId);
console.log(`Broadcast ${finalProgress.status}!`);
```

> **Tip:**
> For large broadcasts, use longer polling intervals (10-30 seconds) to reduce API calls. The progress updates are efficient and don't scan all contacts.

## Status Transitions

```
draft → scheduled → sending → completed
           ↓            ↓
       cancelled    cancelled
```

| Status | `percentComplete` | Description |
|--------|-------------------|-------------|
| `draft` | 0% | Still adding contacts |
| `scheduled` | 0% | Waiting for scheduled time |
| `sending` | 0-99% | Messages being delivered |
| `completed` | 100% | All messages processed |
| `cancelled` | varies | Stopped before completion |

## Viewing Individual Contacts

Each recipient has one of these statuses:

| Status | Meaning |
|--------|---------|
| `pending`, `queued`, `sending` | Not handed to the provider yet |
| `sent` | The provider accepted the message. Delivery is not confirmed yet |
| `delivered` | The channel confirmed delivery to the device. A WhatsApp read receipt also counts |
| `failed` | Not delivered. A `sent` or `delivered` recipient moves here if the provider reports a failure later |
| `skipped` | Not sent: the recipient opted out of the channel, or the broadcast was cancelled first |

Channels that never report delivery leave their recipients at `sent`. Completion counts `sent` recipients as done, so a `completed` broadcast can still have `sent` recipients that move to `delivered` or `failed` afterwards.

Get detailed status for each contact:

```typescript
// Get failed contacts
const failed = await zavu.broadcasts.contacts.list(broadcastId, {
  status: "failed",
  limit: 100,
});

for (const contact of failed.items) {
  console.log(`${contact.recipient}: ${contact.errorMessage}`);
}
```

### Contact Status Response

```json
{
  "items": [
    {
      "id": "bc_abc123",
      "recipient": "+14155551234",
      "status": "failed",
      "errorCode": "30003",
      "errorMessage": "Unreachable destination",
      "processedAt": "2024-01-15T10:35:00.000Z"
    }
  ],
  "nextCursor": null
}
```

## Broadcast Analytics

After completion, get final statistics:

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

console.log("=== Broadcast Report ===");
console.log(`Name: ${broadcast.name}`);
console.log(`Status: ${broadcast.status}`);
console.log(`Total: ${broadcast.totalContacts}`);
console.log(`Delivered: ${broadcast.deliveredCount}`);
console.log(`Failed: ${broadcast.failedCount}`);
console.log(`Delivery Rate: ${(broadcast.deliveredCount / broadcast.totalContacts * 100).toFixed(1)}%`);
console.log(`Duration: ${calculateDuration(broadcast.startedAt, broadcast.status_changedAt)}`);
console.log(`Actual Cost: $${broadcast.actualCost}`);
```

## Error Analysis

Common failure reasons:

| Error Code | Description | Recommendation |
|------------|-------------|----------------|
| `30003` | Unreachable destination | Invalid or disconnected number |
| `30004` | Message blocked | Content filtered by carrier |
| `30005` | Unknown destination | Number doesn't exist |
| `30006` | Landline destination | Cannot SMS landlines |
| `30007` | Carrier violation | Message rejected by carrier |
| `rate_limited` | Too many messages | Temporary, will retry |
| `insufficient_balance` | Out of credits | Top up balance |

### Export Failed Contacts

```typescript
async function exportFailedContacts(broadcastId: string) {
  const failed: string[] = [];
  let cursor: string | undefined;

  do {
    const result = await zavu.broadcasts.contacts.list(broadcastId, {
      status: "failed",
      limit: 100,
      cursor,
    });

    for (const contact of result.items) {
      failed.push(`${contact.recipient},${contact.errorCode},${contact.errorMessage}`);
    }

    cursor = result.nextCursor ?? undefined;
  } while (cursor);

  // Write to CSV
  const csv = ["recipient,error_code,error_message", ...failed].join("\n");
  fs.writeFileSync("failed_contacts.csv", csv);

  console.log(`Exported ${failed.length} failed contacts`);
}
```

## Real-time Updates with Webhooks

For real-time updates without polling, configure webhooks:

```typescript
// In your webhook handler
app.post("/webhooks/zavu", (req, res) => {
  const event = req.body;

  if (event.type === "broadcast.status_changed") {
    console.log(`Broadcast ${event.data.broadcastId} completed!`);
    console.log(`Delivered: ${event.data.deliveredCount}`);
    console.log(`Failed: ${event.data.failedCount}`);
  }

  res.status(200).send("OK");
});
```

### Broadcast Events

| Event | Description |
|-------|-------------|
| `broadcast.started` | Broadcast started sending |
| `broadcast.status_changed` | All messages processed |
| `broadcast.cancelled` | Broadcast was cancelled |

## Best Practices

  
#### Use appropriate polling intervals

    5 seconds for small broadcasts, 30 seconds for large ones
  
  
#### Handle partial failures

    Some contacts may fail while others succeed - check both counts
  
  
#### Export and analyze failures

    Review failed contacts to improve your contact list quality
  
  
#### Set up webhooks for production

    Avoid continuous polling by using webhook notifications
  

## Complete Monitoring 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 monitorBroadcast(broadcastId: string) {
  console.log(`Monitoring broadcast: ${broadcastId}\n`);

  let lastDelivered = 0;
  let lastFailed = 0;

  while (true) {
    const progress = await zavu.broadcasts.progress(broadcastId);

    // Calculate rate
    const newDelivered = progress.delivered - lastDelivered;
    const newFailed = progress.failed - lastFailed;

    console.log(`[${progress.percentComplete.toFixed(1)}%] ` +
      `Delivered: ${progress.delivered} (+${newDelivered}) | ` +
      `Failed: ${progress.failed} (+${newFailed}) | ` +
      `Pending: ${progress.pending}`);

    lastDelivered = progress.delivered;
    lastFailed = progress.failed;

    if (progress.status === "completed") {
      console.log("\n=== Broadcast Complete ===");
      console.log(`Total: ${progress.total}`);
      console.log(`Delivered: ${progress.delivered} (${(progress.delivered / progress.total * 100).toFixed(1)}%)`);
      console.log(`Failed: ${progress.failed} (${(progress.failed / progress.total * 100).toFixed(1)}%)`);
      break;
    }

    if (progress.status === "cancelled") {
      console.log("\n=== Broadcast Cancelled ===");
      console.log(`Skipped: ${progress.skipped} contacts`);
      break;
    }

    await new Promise(r => setTimeout(r, 5000));
  }
}

monitorBroadcast("brd_abc123");
```
