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

# Dialer API

> Create and run outbound dialer campaigns — preview, progressive, predictive, and agentless modes — with contact lists, live stats, and dispositions

# Dialer API

The Dialer API runs outbound calling campaigns: preview, progressive, and predictive modes for human agents, plus agentless voice broadcast and AI-voice-agent dispatch. You create a campaign, attach contact lists, then either let the pacing engine dial (progressive / predictive) or pull the next contact yourself (preview). Live statistics, local-presence caller-ID coverage, and per-attempt dispositions round out the surface.

**Base path:** `/api/v1/dialer`

**Authentication:** API key (`X-API-Key: dv_live_sk_…`) or a Clerk session (`Authorization: Bearer <token>`).

**Scopes:** read endpoints require `dialer:read`; create / update / delete endpoints require `dialer:write`.

**Response envelope:** every endpoint returns `{ "data": …, "meta": { "request_id", "timestamp" } }`. Errors return `{ "error": { "code", "message" } }` (validation failures also carry `error.issues`).

<Note>
  Outbound calls placed by the dialer egress only through Orbit's wholesale voice path. The caller ID you set on a campaign must be a number your account owns (or one you've verified) — a foreign number is rejected with `422 UNVERIFIED_CALLER_ID` on both create and update.
</Note>

***

## Campaign modes

| `mode`        | Who handles the answered call                         | Pacing                                                                          |
| ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------- |
| `preview`     | A human agent, after reviewing the contact            | Agent pulls each contact via `GET /next-call`, then calls `POST /dial`.         |
| `progressive` | A human agent                                         | One live dial per ready agent. Server-paced.                                    |
| `predictive`  | A human agent                                         | Over-dials by `pacing_ratio`, bounded by the FCC abandon ceiling. Server-paced. |
| `agentless`   | No agent — plays `broadcast_message_url` and hangs up | Server-paced broadcast.                                                         |

`dispatch_mode` is a second axis. The default `human` fronts the call with a live operator. `voice_agent` connects each answered leg to the AI voice agent named by `voice_agent_id`; those campaigns are paced by `POST /campaigns/:id/batch-dispatch` instead of the human-agent scheduler.

***

## Create a campaign

<Note>
  `POST /api/v1/dialer/campaigns`
</Note>

**Scope:** `dialer:write`. Created in `draft` status — activate it later with `PATCH /campaigns/:id`.

<ParamField body="name" type="string" required>
  Human-readable campaign name. 1–200 characters.
</ParamField>

<ParamField body="mode" type="string" required>
  One of `preview`, `progressive`, `predictive`, `agentless`.
</ParamField>

<ParamField body="caller_id_e164" type="string" required>
  Caller ID presented on each dial, in E.164 format. Must be a number your account owns.
</ParamField>

<ParamField body="pacing_ratio" type="number" default="1.2">
  Predictive over-dial ratio. 0.8–3.0.
</ParamField>

<ParamField body="max_abandon_rate" type="number" default="0.03">
  Rolling abandon-rate ceiling. Greater than 0 and at most `0.03` (the FCC 3% cap). See [FCC abandon-rate ceiling](#fcc-abandon-rate-ceiling).
</ParamField>

<ParamField body="target_service_level_seconds" type="number" default="20">
  Predictive pacing target — seconds to answer. 5–120.
</ParamField>

<ParamField body="dispatch_mode" type="string" default="human">
  `human` or `voice_agent`. `voice_agent` requires `voice_agent_id`.
</ParamField>

<ParamField body="voice_agent_id" type="string">
  AI voice agent that answers each leg when `dispatch_mode='voice_agent'`. Must reference an existing, non-archived voice agent in your workspace.
</ParamField>

<ParamField body="concurrency_limit" type="number" default="10">
  Maximum simultaneous in-flight legs a single batch-dispatch invocation may originate. 1–200.
</ParamField>

<ParamField body="agent_skills_required" type="array" default="[]">
  Skills an agent must hold to take this campaign's calls. Accepts a string array (`["billing","french"]`) or per-skill objects (`[{ "skill": "billing", "min_level": 3, "weight": 2 }]`).
</ParamField>

<ParamField body="caller_id_pool" type="array" default="[]">
  Rotation pool of E.164 numbers for local-presence dialing. Every entry must be a number your account owns. Required (non-empty) when `caller_id_strategy` is not `single`.
</ParamField>

<ParamField body="caller_id_strategy" type="string" default="single">
  `single` (always `caller_id_e164`), `npa_match` (present the pool number matching the lead's area code), or `round_robin`.
</ParamField>

<ParamField body="broadcast_message_url" type="string">
  HTTPS audio URL played on answer in `agentless` mode. **Required** for `agentless`; rejected for every other mode.
</ParamField>

<ParamField body="voicemail_message_url" type="string">
  HTTPS audio URL dropped when answering-machine detection fires on an agent-driven dial. Not valid for `agentless` mode.
</ParamField>

<ParamField body="preview_decision_seconds" type="number">
  Per-campaign preview countdown the agent has to accept a contact. Only valid for `preview` mode; omit to use the platform default.
</ParamField>

<ParamField body="recording_enabled" type="boolean" default="false">
  Record calls on this campaign. Requires `recording_consent_acknowledged: true` in the same request.
</ParamField>

<ParamField body="recording_consent_acknowledged" type="boolean">
  Affirms you have authority to record and the two-party-consent disclaimer will play. **Required when `recording_enabled` is true.** Internal API-key callers cannot acknowledge on behalf of an operator (`401`).
</ParamField>

<ParamField body="amd_enabled" type="boolean" default="false">
  Request carrier answering-machine detection on each dial.
</ParamField>

<ParamField body="application_sid" type="string">
  Voice application (UUID) bound to answered legs.
</ParamField>

<ParamField body="retry_policy" type="object" default="{}">
  Per-outcome retry backoff keyed by outcome bucket (`no_answer`, `busy`, `machine`, `machine_silence`, `failed`). Each entry sets retry delays (60–86400s) and `max_attempts` (0–10). Empty `{}` keeps the platform's uniform retry schedule.
</ParamField>

<ParamField body="campaign_script" type="object">
  Human-agent guided-dialog script (ordered sections plus per-disposition branches). Omit to store the canonical empty script `{ "version": 1, "sections": [], "branches": {} }`.
</ParamField>

<ParamField body="metadata" type="object" default="{}">
  Arbitrary key-value pairs stored on the campaign.
</ParamField>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/dialer/campaigns" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Q3 outbound — US East",
    "mode": "predictive",
    "caller_id_e164": "+12025551234",
    "pacing_ratio": 1.2,
    "max_abandon_rate": 0.03,
    "recording_enabled": true,
    "recording_consent_acknowledged": true
  }'
```

**201 Created**

```json theme={null}
{
  "data": {
    "id": "dcmp_abc",
    "name": "Q3 outbound — US East",
    "mode": "predictive",
    "status": "draft",
    "caller_id_e164": "+12025551234",
    "pacing_ratio": 1.2,
    "max_abandon_rate": 0.03,
    "dispatch_mode": "human",
    "concurrency_limit": 10,
    "caller_id_strategy": "single",
    "campaign_script": { "version": 1, "sections": [], "branches": {} },
    "recording_enabled": true,
    "recording_consent_acknowledged_at": "2026-06-18T12:00:00Z",
    "recording_consent_acknowledged_by": "user_123"
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-06-18T12:00:00Z" }
}
```

**Errors**

| Status | `error.code`                 | Cause                                                                                                                                                                                       |
| ------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `RECORDING_CONSENT_REQUIRED` | `recording_consent_acknowledged: true` sent by a non-operator (API-key) caller.                                                                                                             |
| `422`  | `VALIDATION_ERROR`           | Schema failure — `max_abandon_rate` above `0.03`, `agentless` without `broadcast_message_url`, `voice_agent` without `voice_agent_id`, unknown `voice_agent_id`, recording without consent. |
| `422`  | `UNVERIFIED_CALLER_ID`       | `caller_id_e164` or a `caller_id_pool` entry is a number your account doesn't own and hasn't verified as an outbound caller ID.                                                             |
| `503`  | `TENANT_SCHEMA_INCOMPLETE`   | Workspace not yet provisioned for the dialer.                                                                                                                                               |

***

## List campaigns

<Note>
  `GET /api/v1/dialer/campaigns`
</Note>

**Scope:** `dialer:read`. Cursor-paginated, newest first. Soft-deleted campaigns are excluded.

**Query parameters**

| Name     | Type            | Notes                                                                                                         |
| -------- | --------------- | ------------------------------------------------------------------------------------------------------------- |
| `limit`  | integer (1–100) | Default 20.                                                                                                   |
| `cursor` | string          | Opaque cursor from the previous response's `next_cursor`.                                                     |
| `status` | string          | Optional filter: `draft`, `active`, `paused`, `completed`, `aborted`. Unknown values are ignored (no filter). |

Each row carries a bounded `contacts_total` / `contacts_connected` rollup. When a campaign's contact count exceeds the scan cap, `contacts_count_capped` is `true` and the totals are a floor; the envelope's `contacts_count_capped_at` reports where the count stops being exact.

```json theme={null}
{
  "data": {
    "items": [
      {
        "id": "dcmp_abc",
        "name": "Q3 outbound — US East",
        "mode": "predictive",
        "status": "active",
        "caller_id_e164": "+12025551234",
        "pacing_ratio": 1.2,
        "max_abandon_rate": 0.03,
        "abandon_rate_30d": 0.018,
        "contacts_total": 4120,
        "contacts_connected": 386,
        "contacts_count_capped": false,
        "campaign_script": { "version": 1, "sections": [], "branches": {} },
        "created_at": "2026-06-18T10:30:00Z"
      }
    ],
    "next_cursor": "MjAyNi0wNi0xOFQxMDozMDowMC4wMDBa",
    "has_more": false,
    "contacts_count_capped_at": 10000
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-06-18T12:00:00Z" }
}
```

<Note>
  On a workspace that has never created a campaign — or one provisioned before the dialer shipped — this endpoint returns an empty list (`items: []`) rather than an error, so an empty-state UI renders cleanly.
</Note>

***

## Campaign stats summary

<Note>
  `GET /api/v1/dialer/campaigns/stats`
</Note>

**Scope:** `dialer:read`. Totals across **every** non-deleted campaign — the source of truth for a dashboard KPI strip, independent of pagination.

**Query parameters**

| Name | Type   | Notes                                                                                                   |
| ---- | ------ | ------------------------------------------------------------------------------------------------------- |
| `tz` | string | IANA timezone (e.g. `America/New_York`) used for the `completed_today` day boundary. Defaults to `UTC`. |

```json theme={null}
{
  "data": {
    "active": 3,
    "paused": 1,
    "completed_today": 2,
    "avg_abandon_rate": 0.021
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-06-18T12:00:00Z" }
}
```

`avg_abandon_rate` is `null` when no active campaign carries a 30-day rate.

***

## Get a campaign

<Note>
  `GET /api/v1/dialer/campaigns/{id}`
</Note>

**Scope:** `dialer:read`. Returns the full campaign row; `campaign_script` is always the canonical shape.

**Errors**

| Status | `error.code` | Cause                               |
| ------ | ------------ | ----------------------------------- |
| `404`  | `NOT_FOUND`  | Campaign not found or soft-deleted. |

***

## Update a campaign

<Note>
  `PATCH /api/v1/dialer/campaigns/{id}`
</Note>

**Scope:** `dialer:write`. Partial update — only the fields you send change. Accepts the same fields as create, plus `status`.

<ParamField body="status" type="string">
  `active`, `paused`, `completed`, or `aborted`. `aborted` is the operator-initiated terminal state — once aborted, a campaign cannot be re-activated. Transitioning to `active` is blocked while your workspace's compliance emergency-stop is engaged.
</ParamField>

Nullable PATCH fields: `voice_agent_id`, `voicemail_message_url`, `preview_decision_seconds`, and `call_script_id` accept `null` to clear them. Sending `caller_id_e164` or any `caller_id_pool` entry re-runs the ownership check. Sending `retry_policy` or `campaign_script` replaces the whole object.

```bash cURL theme={null}
# Activate a campaign
curl -X PATCH "https://api.orbit.devotel.io/api/v1/dialer/campaigns/dcmp_abc" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "status": "active" }'
```

**Errors**

| Status | `error.code`                         | Cause                                                                                                                                                            |
| ------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `RECORDING_CONSENT_REQUIRED`         | `recording_consent_acknowledged: true` sent by a non-operator (API-key) caller. Recording a call needs an operator identity to attribute the acknowledgement to. |
| `403`  | `ORG_COMPLIANCE_EMERGENCY_STOP`      | Tried to activate while the workspace compliance kill switch is on.                                                                                              |
| `404`  | `NOT_FOUND`                          | Campaign not found or soft-deleted.                                                                                                                              |
| `409`  | `CAMPAIGN_ABORTED`                   | Tried to move `status` away from `aborted`. `aborted` is terminal — create a new campaign instead.                                                               |
| `409`  | `RECORDING_CONSENT_REQUIRED`         | Activating a campaign that records calls but has no consent acknowledgement on file. Send `recording_consent_acknowledged: true` in the same request.            |
| `422`  | `VALIDATION_ERROR`                   | Schema failure (same rules as create), e.g. `max_abandon_rate` above `0.03`, or a `voice_agent_id` that doesn't reference an existing voice agent.               |
| `422`  | `UNVERIFIED_CALLER_ID`               | A changed `caller_id_e164` or `caller_id_pool` entry is a number your account doesn't own and hasn't verified.                                                   |
| `422`  | `VOICEMAIL_DROP_FORBIDDEN_AGENTLESS` | Set `voicemail_message_url` on an `agentless` campaign. Voicemail drop applies only to agent-driven modes; agentless campaigns use `broadcast_message_url`.      |
| `422`  | `VOICE_AGENT_MISSING`                | The update would leave `dispatch_mode: 'voice_agent'` with no `voice_agent_id`. Set one in the same request, or switch `dispatch_mode` back to `human`.          |
| `503`  | `TENANT_SCHEMA_INCOMPLETE`           | Workspace not yet provisioned for the dialer.                                                                                                                    |

***

## Soft-delete a campaign

<Note>
  `DELETE /api/v1/dialer/campaigns/{id}`
</Note>

**Scope:** `dialer:write`. Soft-deletes the campaign and flips its pending contacts to `archived`. Terminal contacts (`connected` / `failed` / `dnc` / `exhausted` / `wrong_number`) are preserved untouched as a compliance audit trail. Campaign, lists, and contacts commit in one transaction.

**Errors**

| Status | `error.code`                | Cause                                                                                                                                   |
| ------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `404`  | `NOT_FOUND`                 | Campaign not found or already soft-deleted.                                                                                             |
| `409`  | `DIALER_CAMPAIGN_IN_FLIGHT` | A contact is currently `dialing`. Pause the campaign, let live legs finish, then retry. `details.in_flight_contacts` returns the count. |

***

## List contact lists

<Note>
  `GET /api/v1/dialer/campaigns/{id}/lists`
</Note>

**Scope:** `dialer:read`. Cursor-paginated. Each list carries `total_contacts` and a live `contacts_dialing` badge.

**Query parameters**

| Name     | Type            | Notes                                                     |
| -------- | --------------- | --------------------------------------------------------- |
| `limit`  | integer (1–100) | Default 20.                                               |
| `cursor` | string          | Opaque cursor from the previous response's `next_cursor`. |

```json theme={null}
{
  "data": {
    "items": [
      {
        "id": "dlst_abc",
        "name": "June leads — East",
        "total_contacts": 2000,
        "contacts_dialing": 4,
        "status": "active",
        "created_at": "2026-06-18T10:30:00Z",
        "updated_at": "2026-06-18T11:00:00Z"
      }
    ],
    "next_cursor": null,
    "has_more": false
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-06-18T12:00:00Z" }
}
```

***

## Upload a contact list

<Note>
  `POST /api/v1/dialer/campaigns/{id}/lists`
</Note>

**Scope:** `dialer:write`. Attaches a new contact list to the campaign and enqueues its contacts.

<ParamField body="name" type="string" required>
  List name. 1–200 characters.
</ParamField>

<ParamField body="contacts" type="array" required>
  Contacts to dial. At least one. Each entry: `phone_e164` (required, E.164), `display_name` (optional, ≤200 chars), `contact_id` (optional, links to a CRM contact). Duplicate `phone_e164` values within the upload are de-duplicated (first occurrence wins) and reported back.
</ParamField>

<ParamField body="max_attempts_per_contact" type="number" default="5">
  Maximum dial attempts before a contact is exhausted. 1–20.
</ParamField>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/dialer/campaigns/dcmp_abc/lists" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "June leads — East",
    "max_attempts_per_contact": 5,
    "contacts": [
      { "phone_e164": "+14155552671", "display_name": "Jane Doe", "contact_id": "con_abc" }
    ]
  }'
```

**Errors**

| Status | `error.code`       | Cause                                                                       |
| ------ | ------------------ | --------------------------------------------------------------------------- |
| `404`  | `NOT_FOUND`        | Campaign not found or soft-deleted.                                         |
| `422`  | `VALIDATION_ERROR` | Empty `contacts`, more than the per-upload cap, or an invalid `phone_e164`. |

***

## Soft-delete a contact list

<Note>
  `DELETE /api/v1/dialer/campaigns/{id}/lists/{listId}`
</Note>

**Scope:** `dialer:write`. Soft-deletes one list within the campaign. Pending contacts in the list flip to `archived`; terminal contacts are preserved as audit trail.

**Errors**

| Status | `error.code`            | Cause                                                          |
| ------ | ----------------------- | -------------------------------------------------------------- |
| `404`  | `NOT_FOUND`             | List not found, not in this campaign, or already soft-deleted. |
| `409`  | `DIALER_LIST_IN_FLIGHT` | A contact in the list is currently `dialing`.                  |

***

## Campaign live stats

<Note>
  `GET /api/v1/dialer/campaigns/{id}/stats`
</Note>

**Scope:** `dialer:read`. Wallboard detail for one campaign: per-status contact breakdown, a 24-hour call-outcome rollup with average handle time, and a rolling 30-minute abandonment rate that mirrors what the pacing engine throttles against.

```json theme={null}
{
  "data": {
    "id": "dcmp_abc",
    "name": "Q3 outbound — US East",
    "mode": "predictive",
    "status": "active",
    "pacing_ratio": 1.2,
    "abandon_rate_30d": 0.018,
    "max_abandon_rate": 0.03,
    "contacts_pending": 1500,
    "contacts_dialing": 6,
    "contacts_connected": 386,
    "contacts_failed": 120,
    "contacts_dnc": 14,
    "contacts_timezone_blocked": 40,
    "contacts_exhausted": 80,
    "contacts_archived": 0,
    "contacts_callback_scheduled": 22,
    "contacts_wrong_number": 9,
    "contacts_total": 2177,
    "contacts_count_capped": false,
    "last_24h": {
      "calls_connected": 410,
      "calls_no_answer": 220,
      "calls_busy": 35,
      "calls_voicemail": 60,
      "calls_amd_detected": 18,
      "calls_abandoned": 7,
      "avg_handle_time_seconds": 142.4,
      "total_attempts": 750
    },
    "last_30m": {
      "abandoned_count": 1,
      "connected_count": 48,
      "abandon_rate_30m": 0.0208
    }
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-06-18T12:00:00Z" }
}
```

`avg_handle_time_seconds` is `null` until a connected call has terminated. `contacts_count_capped` is `true` when the contact set exceeds the scan cap (the totals are then a floor).

**Errors**

| Status | `error.code` | Cause                               |
| ------ | ------------ | ----------------------------------- |
| `404`  | `NOT_FOUND`  | Campaign not found or soft-deleted. |

***

## Caller-ID coverage

<Note>
  `GET /api/v1/dialer/campaigns/{id}/caller-id-coverage`
</Note>

**Scope:** `dialer:read`. Local-presence analytics for one campaign: which destination area codes (NPAs) your caller-ID pool can present locally, the rolling answer rate per destination NPA, and the highest-volume under-covered NPAs to add a local number for. Read-only; originates no calls. North American Numbering Plan (`+1`) numbers only.

**Errors**

| Status | `error.code` | Cause                               |
| ------ | ------------ | ----------------------------------- |
| `404`  | `NOT_FOUND`  | Campaign not found or soft-deleted. |

***

## Voice-agent campaign rollup

<Note>
  `GET /api/v1/dialer/voice-agents/{agentId}/campaigns`
</Note>

**Scope:** `dialer:read`. Lists the `dispatch_mode='voice_agent'` campaigns bound to one AI voice agent, with a per-campaign contact and call-outcome rollup over a rolling window. Cursor-paginated (`limit` / `cursor`, `next_cursor` / `has_more`). Human-mode campaigns never appear here. The envelope discloses `contacts_count_capped_at` and `rollup_window_days`. Returns a zeroed rollup (`campaign_count: 0`) for an unprovisioned workspace rather than an error.

***

## Submit a disposition

<Note>
  `POST /api/v1/dialer/campaigns/{id}/dispositions`
</Note>

**Scope:** `dialer:write`. An agent submits the outcome of a dial attempt; the platform routes the contact's lifecycle status from the disposition (for example `do_not_call` adds the number to your do-not-call list, `callback_later` reschedules the contact).

<ParamField body="attempt_id" type="string" required>
  The call-attempt id returned when the dial leg was placed.
</ParamField>

<ParamField body="disposition" type="string" required>
  Agent-selected disposition string (1–100 chars). Matched case-insensitively against the disposition matrix.
</ParamField>

<ParamField body="callback_scheduled_at" type="string">
  ISO-8601 timestamp with offset. **Required when the disposition reschedules a callback** (`callback_later` / `callback`). Must be in the future and at most 90 days ahead.
</ParamField>

<ParamField body="notes" type="string">
  Free-form agent notes, up to 2000 characters.
</ParamField>

The full disposition-to-status matrix and side effects (do-not-call writes, callback scheduling, connect-rate accounting) are documented on the [Dialer Dispositions](/voice/dialer-dispositions) page.

**Errors**

| Status | `error.code`       | Cause                                                                                               |
| ------ | ------------------ | --------------------------------------------------------------------------------------------------- |
| `404`  | `NOT_FOUND`        | `attempt_id` not found in this campaign.                                                            |
| `422`  | `VALIDATION_ERROR` | `callback_scheduled_at` missing on a callback disposition, in the past, or more than 90 days ahead. |

***

## Get the next contact (preview mode)

<Note>
  `GET /api/v1/dialer/next-call?campaign_id=dcmp_abc`
</Note>

**Scope:** `dialer:read`. For **preview-mode** campaigns: atomically claims the next eligible contact so two agents polling at once never receive the same row. Progressive / predictive campaigns return `204 No Content` — those are server-paced. A preview contact stuck `dialing` for more than five minutes (agent abandoned the tab) becomes re-claimable.

```json theme={null}
{
  "data": {
    "contact": {
      "id": "dlc_xyz",
      "phone_e164": "+14155552671",
      "display_name": "Jane Doe",
      "contact_id": "con_abc",
      "attempts": 0
    }
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-06-18T12:00:00Z" }
}
```

**204 No Content** — the campaign is `progressive` or `predictive` (the pacing engine handles dialing).

**Errors**

| Status | `error.code`  | Cause                                  |
| ------ | ------------- | -------------------------------------- |
| `400`  | `BAD_REQUEST` | `campaign_id` query parameter missing. |
| `404`  | `NOT_FOUND`   | Campaign not found or not `active`.    |

***

## Place a preview dial

<Note>
  `POST /api/v1/dialer/dial`
</Note>

**Scope:** `dialer:write`. Originates the dial leg for a contact the agent claimed via `GET /next-call`. Only valid for `active` preview campaigns; progressive / predictive campaigns dial through the pacing engine. Requires an authenticated operator — an API-key caller cannot dial on behalf of an agent (`401 AGENT_REQUIRED`).

<ParamField body="campaign_id" type="string" required>
  The preview campaign to dial within.
</ParamField>

<ParamField body="contact_id" type="string" required>
  The contact (from `GET /next-call`) to dial.
</ParamField>

**Errors**

| Status | `error.code`               | Cause                                                                                                                                                                |
| ------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `AGENT_REQUIRED`           | Called by a non-operator (API-key) caller.                                                                                                                           |
| `404`  | `NOT_FOUND`                | No active campaign, or the contact does not exist.                                                                                                                   |
| `409`  | `WRONG_DIALER_MODE`        | Campaign is not in preview mode (other modes dial via the pacing engine).                                                                                            |
| `409`  | `CLAIM_NOT_HELD`           | The contact is no longer in `dialing` state — re-fetch via `GET /next-call` first.                                                                                   |
| `422`  | `VALIDATION_ERROR`         | Missing `campaign_id` / `contact_id`.                                                                                                                                |
| `503`  | `SERVICE_UNAVAILABLE`      | The dialer's voice control-plane application is not configured. Contact support.                                                                                     |
| `503`  | `SOFTSWITCH_UNHEALTHY`     | Outbound voice is failing above the health threshold; dispatch is paused and the contact claim is released. Re-fetch via `GET /next-call` to retry once it recovers. |
| `503`  | `TENANT_SCHEMA_INCOMPLETE` | Workspace not yet provisioned for the dialer.                                                                                                                        |

***

## Batch-dispatch voice-agent legs

<Note>
  `POST /api/v1/dialer/campaigns/{id}/batch-dispatch`
</Note>

**Scope:** `dialer:write`. Paces a `dispatch_mode='voice_agent'` campaign: originates up to the campaign's `concurrency_limit` simultaneous AI-voice-agent legs. Each answered leg connects to the campaign's `voice_agent_id`.

<ParamField body="max_legs" type="number">
  Optional per-invocation cap on legs to originate. 1–200. Clamped to the campaign's `concurrency_limit` (the effective cap is the smaller of the two).
</ParamField>

A successful call returns one of three statuses:

* **202 Accepted** — every claimed leg was originated.
* **207 Multi-Status** — some legs originated and some failed. Inspect each `legs[].ok` for the per-leg result.
* **204 No Content** — no eligible contacts were pending, so nothing was dispatched on this tick.

**202 Accepted / 207 Multi-Status**

```json theme={null}
{
  "data": {
    "campaign_id": "dcmp_abc",
    "voice_agent_id": "agt_xyz",
    "claimed": 10,
    "legs": [
      {
        "contact_id": "dlc_1",
        "attempt_id": "dialerAttempt_1",
        "jambonz_call_sid": "call_abc",
        "ok": true
      },
      {
        "contact_id": "dlc_2",
        "attempt_id": "dialerAttempt_2",
        "jambonz_call_sid": null,
        "ok": false,
        "error": "compliance_deferred:TCPA_DIALING_WINDOW_BLOCKED"
      }
    ]
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-06-18T12:00:00Z" }
}
```

**204 No Content** — no pending contacts to dispatch on this tick.

**Errors**

| Status | `error.code`               | Cause                                                                                                                                        |
| ------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `BAD_REQUEST`              | Campaign id missing.                                                                                                                         |
| `404`  | `NOT_FOUND`                | Campaign not found.                                                                                                                          |
| `409`  | `WRONG_DISPATCH_MODE`      | Campaign's `dispatch_mode` is not `voice_agent`. Human campaigns dial via `/dial` (preview) or the pacing engine (progressive / predictive). |
| `409`  | `VOICE_AGENT_MISSING`      | `dispatch_mode` is `voice_agent` but no `voice_agent_id` is set. PATCH one onto the campaign first.                                          |
| `409`  | `CAMPAIGN_NOT_ACTIVE`      | Campaign status is not `active`. Activate it with `PATCH /campaigns/:id` before dispatching.                                                 |
| `422`  | `VALIDATION_ERROR`         | `max_legs` out of range.                                                                                                                     |
| `503`  | `SERVICE_UNAVAILABLE`      | The dialer's voice control-plane application is not configured. Contact support.                                                             |
| `503`  | `SOFTSWITCH_UNHEALTHY`     | Outbound voice is failing above the health threshold; the batch is paused and every claimed contact is released back to pending.             |
| `503`  | `TENANT_SCHEMA_INCOMPLETE` | Workspace not yet provisioned for the dialer.                                                                                                |

***

## FCC abandon-rate ceiling

`max_abandon_rate` is capped at **0.03 (3%)** per the FCC Telemarketing Sales Rule. Any create or update that sends a higher value is rejected with `422 VALIDATION_ERROR`. The pacing engine also auto-flips a campaign to `status: 'aborted'` if its rolling 30-day abandon rate trips the ceiling. `aborted` is terminal and cannot be re-activated — review the abort reason and create a new campaign.

## See also

* [Dialer Dispositions](/voice/dialer-dispositions) — the full disposition matrix and contact-status enum
* [Voice API](/api-reference/voice) — calls, SIP trunks, IVR, conferences
* [Numbers API](/api-reference/numbers) — buy and manage the DIDs you present as caller ID
