> This page is for Voice AI Platform.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.hams.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.hams.ai/_mcp/server.

# Outbound batches

> Upload leads, schedule and publish a batch, and follow each lead to its outcome.

A batch is a list of phone numbers, the leads, that the platform calls on your behalf with one of
your AI agents, from one of your telephony extensions. You create it and publish it; the
[dialer](/platform/outbound-dialer) does the calling. Along the way you can watch every lead change
status, stop and resume the batch, and add more leads.

Batches are managed with a secret key; changes need a key with the `WRITER` role.

## Before you start

A batch dials from a telephony extension, and the extension decides which agent takes the calls.
Set the extension's outbound agent in the dashboard under **Cloud Call Center > Settings >
Extensions** first. Creating a batch on an extension without one fails with `400` and
`"No outbound agent configured on this extension"`.

## Create a batch

```bash
curl -X POST https://api.hams.ai/outbound-batch/batches \
  -H "X-Secret-Key: $HAMS_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Renewal reminders, September",
    "extension_id": "ext_…",
    "scheduled_date": "2026-09-01T06:00:00Z",
    "fetch_limit": 20,
    "max_attempts_per_lead": 3,
    "is_draft": true,
    "leads": [
      { "number": "+966500000001", "dynamic_variables": { "customer_name": "Sara", "renewal_date": "5 September" } },
      { "number": "+966500000002", "dynamic_variables": { "customer_name": "Omar", "renewal_date": "7 September" } }
    ],
    "webhook": {
      "url": "https://example.com/hooks/hams",
      "method": "POST",
      "events": ["outbound_lead_status_changed"]
    }
  }'
```

You get `201` and the batch:

```json
{
  "id": "…",
  "entity_id": "…",
  "name": "Renewal reminders, September",
  "extension_id": "ext_…",
  "agent_id": "…",
  "display_number": null,
  "format_number": "none",
  "status": "DRAFT",
  "scheduled_date": "2026-09-01T06:00:00",
  "fetch_limit": 20,
  "max_attempts_per_lead": 3,
  "lead_count": 2,
  "created": "…",
  "updated": "…",
  "webhook": { "url": "https://example.com/hooks/hams", "method": "POST", "events": ["outbound_lead_status_changed"], "…": "…" }
}
```

| Field                   | Notes                                                                                                                                                                                        |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                  | 1–255 characters                                                                                                                                                                             |
| `extension_id`          | The extension to call from; its outbound agent takes the calls and is copied to `agent_id`                                                                                                   |
| `scheduled_date`        | When calling may start. Must be in the future; sent with a timezone or taken as UTC                                                                                                          |
| `fetch_limit`           | Leads the dialer claims from this batch per pass; default `20`                                                                                                                               |
| `max_attempts_per_lead` | Stored with the batch; default `3`. The current dialer makes one attempt per lead                                                                                                            |
| `leads`                 | 1–10,000 per request. `number` is digits with an optional leading `+`, 7–30 characters; `dynamic_variables` is a map of strings the agent can use, on top of its `default_dynamic_variables` |
| `webhook`               | Optional. `url`, `method`, `events` (only `outbound_lead_status_changed` for batches), optional `headers`, `max_retries`                                                                     |
| `is_draft`              | `true` creates the batch as `DRAFT` so you can review it before publishing; default `false` creates it `ACTIVE`                                                                              |

> **Note**
>
> Send leads in the format your trunk expects. Numbers are stored as given; `format_number` is
> `none` unless the extension's routing changes it.

## Publish, stop, reopen

| Call                                              | Effect                                                                                                                                                           |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /outbound-batch/batches/{batch_id}/publish` | `DRAFT` → `ACTIVE`; the dialer starts claiming leads once `scheduled_date` has passed. Returns `{ "status": "ACTIVE" }`                                          |
| `POST /outbound-batch/batches/{batch_id}/stop`    | `ACTIVE` → `STOPPED`; leads already dialing finish and get their outcome. Returns `{ "status": "STOPPED" }`                                                      |
| `POST /outbound-batch/batches/{batch_id}/leads`   | Appends leads (`{ "leads": [ … ] }`). A `STOPPED` or `COMPLETED` batch reopens to `ACTIVE`; a `DRAFT` stays a draft. Returns `leads_added` and `previous_status` |
| `DELETE /outbound-batch/batches/{batch_id}`       | Marks the batch deleted; it disappears from lists and is no longer dialed                                                                                        |

### Batch status

| Status      | Meaning                               |
| ----------- | ------------------------------------- |
| `DRAFT`     | Created with `is_draft`, never dialed |
| `ACTIVE`    | The dialer claims leads from it       |
| `STOPPED`   | Paused by you; the dialer skips it    |
| `COMPLETED` | Every lead reached a final status     |
| `deleted`   | Removed                               |

### Lead status

| Status      | Meaning                                                                              |
| ----------- | ------------------------------------------------------------------------------------ |
| `PENDING`   | Waiting to be dialed                                                                 |
| `DIALING`   | The call is in flight                                                                |
| `ANSWERED`  | Answered and handled by the agent; `end_reason` says who hung up, `USER` or `SYSTEM` |
| `NO_ANSWER` | Nobody picked up                                                                     |
| `BUSY`      | The line was busy                                                                    |
| `FAILED`    | The call could not be placed                                                         |

## Follow progress

```bash
curl "https://api.hams.ai/outbound-batch/batches?status=ACTIVE" \
  -H "X-Secret-Key: $HAMS_SECRET_KEY"
```

```json
{ "success": true, "data": [ { "id": "…", "name": "…", "status": "ACTIVE", "lead_count": 2, "…": "…" } ], "count": 1, "total": 1, "limit": 100, "offset": 0 }
```

`GET /outbound-batch/batches/{batch_id}` returns one batch with its `webhook`, and
`GET /outbound-batch/batches/{batch_id}/leads` pages through its leads in the same envelope:

```json
{
  "success": true,
  "data": [
    { "id": "…", "batch_id": "…", "number": "+966500000001", "dynamic_variables": { "customer_name": "Sara" }, "status": "ANSWERED", "created": "…", "updated": "…" }
  ],
  "count": 1, "total": 2, "limit": 100, "offset": 0
}
```

If the batch has a webhook, `outbound_lead_status_changed` is delivered to it each time a lead moves
to a new status, so you do not have to poll.