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

> Outbound dialer disposition matrix — how agent-submitted dispositions route the contact's lifecycle status

# Dialer Dispositions

When an agent submits a disposition on an outbound dial-attempt, the platform routes the contact's lifecycle status based on a canonical mapping. The correct route is critical: a `do_not_call` disposition that's mis-routed leaves the contact in the active pool, which creates a TCPA exposure (47 U.S.C. § 227(b)(3) carries a private right of action with $500–$1,500 statutory damages per call).

This page documents the disposition matrix and the `POST /campaigns/:id/dispositions` endpoint that agents call to submit them.

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

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

**Scope:** `dialer:write` on every endpoint listed here.

***

## Contact-status enum

`dialer_list_contacts.status` is the contact lifecycle. An agent disposition routes the contact to one of these statuses:

| Status               | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`            | Fresh contact, eligible for the next pacing claim once `next_attempt_at <= now()`.                                                                                                                                                                                                                                                                                                                                                                                                |
| `dialing`            | Currently being dialed (or claimed by an agent in preview mode). Atomic flip via `FOR UPDATE SKIP LOCKED`.                                                                                                                                                                                                                                                                                                                                                                        |
| `connected`          | Talked, voicemail dropped, or general "outcome captured" (`sale` / `no_sale` / `voicemail` / `interested` / `not_interested`). Terminal for the current attempt; the contact may be re-queued if `attempts < max_attempts`. Note: `busy` / `no_answer` / carrier failures route to `failed` (PS-STATE\_MACHINE-12), **not** `connected`.                                                                                                                                          |
| `callback_scheduled` | Agent scheduled a callback. `next_attempt_at` is set to the requested ISO timestamp; pacing claim re-picks the contact at that moment.                                                                                                                                                                                                                                                                                                                                            |
| `wrong_number`       | Number was reached but the called party is not the intended contact. Terminal — but **not** added to the DNC list (the rightful owner may still be contactable on reassignment).                                                                                                                                                                                                                                                                                                  |
| `dnc`                | Do-not-call. Terminal. The phone is INSERTed into the tenant `dnc_list` so any future pacing tick across any campaign blocks at the compliance gate (`checkDialerCompliance` → `checkDnc`).                                                                                                                                                                                                                                                                                       |
| `timezone_blocked`   | The TCPA quiet-hours / unresolved-timezone guard blocked the dial — the contact's local time is outside the permitted calling window or its timezone could not be resolved. Not terminal: the contact is deferred and becomes eligible again once the local window opens or the timezone resolves on a later pacing tick.                                                                                                                                                         |
| `exhausted`          | Max attempts consumed without a `connected` outcome. Terminal.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `archived`           | Soft-deleted via `DELETE /campaigns/:id` or `DELETE /campaigns/:id/lists/:listId`. Audit-trail only.                                                                                                                                                                                                                                                                                                                                                                              |
| `failed`             | Carrier-level failure (invalid number, network error) or any non-connect outcome — the routed status for `busy` / `no_answer` / `failed` / `fax_machine_detected` / `operator_intercept` dispositions, and for any unmapped disposition (PS-STATE\_MACHINE-12). Excluded from connect-rate maths. The per-leg `busy` / `no_answer` signal is preserved on `dialer_call_attempts.outcome` and the verbatim string on `dialer_call_attempts.disposition`. Terminal for the attempt. |

***

## Disposition → status matrix

Disposition matching is **case-insensitive and trim-stripped** at the request boundary. Any disposition not listed below routes to `failed` (PS-STATE\_MACHINE-12), **not** `connected` — unmapped dispositions are deliberately excluded from connect-rate maths so a freeform or newly-added FE disposition cannot silently inflate the connect-rate KPI or pollute the TCPA per-attempt audit. The agent's chosen string is still persisted verbatim on `dialer_call_attempts.disposition`; operators adding a new disposition should extend the matrix so it routes intentionally instead of defaulting to `failed`.

| Disposition string     | Routes to status     | Side effects                                                                                                                                                                 |
| ---------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `callback_later`       | `callback_scheduled` | Requires `callback_scheduled_at` in body. Stamps `next_attempt_at` on the contact.                                                                                           |
| `callback`             | `callback_scheduled` | Same as `callback_later`.                                                                                                                                                    |
| `wrong_number`         | `wrong_number`       | Terminal — no DNC write.                                                                                                                                                     |
| `dnc`                  | `dnc`                | INSERT INTO tenant `dnc_list` (phone, source=`dialer_disposition`, reason=disposition). Idempotent on phone.                                                                 |
| `do_not_call`          | `dnc`                | Same as `dnc`.                                                                                                                                                               |
| `sale`                 | `connected`          | None beyond the status flip.                                                                                                                                                 |
| `no_sale`              | `connected`          | None.                                                                                                                                                                        |
| `voicemail`            | `connected`          | None. The voicemail leg is already captured on `dialer_call_attempts.outcome`.                                                                                               |
| `interested`           | `connected`          | None.                                                                                                                                                                        |
| `not_interested`       | `connected`          | None.                                                                                                                                                                        |
| `busy`                 | `failed`             | None. Non-connect outcome (PS-STATE\_MACHINE-12) — excluded from connect-rate maths, not routed to `connected`. The `busy` signal is kept on `dialer_call_attempts.outcome`. |
| `no_answer`            | `failed`             | None. Non-connect outcome (PS-STATE\_MACHINE-12) — excluded from connect-rate maths. The `no_answer` signal is kept on `dialer_call_attempts.outcome`.                       |
| `failed`               | `failed`             | None. Non-connect outcome (PS-STATE\_MACHINE-12) — excluded from connect-rate maths.                                                                                         |
| `fax_machine_detected` | `failed`             | None. Non-connect outcome (PS-STATE\_MACHINE-12) — routed to `failed`, excluded from connect-rate maths.                                                                     |
| `operator_intercept`   | `failed`             | None. Non-connect outcome (PS-STATE\_MACHINE-12) — routed to `failed`, excluded from connect-rate maths.                                                                     |
| *any other string*     | `failed`             | None. The literal disposition string is still persisted on `dialer_call_attempts.disposition`. Excluded from connect-rate maths (PS-STATE\_MACHINE-12).                      |

<Note>
  **DNC writes are platform-wide per tenant.** A `dnc` disposition in Campaign A blocks future dials to the same phone in Campaign B, C, etc. on the same tenant — the DNC list is read by every pacing tick across every campaign in the tenant. Inserts are idempotent (UNIQUE on `phone`), so the first-write wins and existing entries' `source` / `reason` are preserved.
</Note>

***

## Submit disposition

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

**Scope:** `dialer:write`.

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

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

<ParamField body="notes" type="string">
  Free-form agent notes. Max 2000 chars. **Accepted but not currently persisted** — the field is validated at the request boundary but is not yet written to the attempt (no storage column has landed). Treat it as a no-op until a future release adds persistence; do not rely on it surviving the request.
</ParamField>

<ParamField body="callback_scheduled_at" type="string">
  ISO-8601 timestamp with timezone offset. **REQUIRED when disposition routes to `callback_scheduled`** (`callback_later` / `callback`). Must be strictly in the future and ≤90 days ahead.
</ParamField>

```bash cURL theme={null}
# Schedule a callback
curl -X POST "https://api.orbit.devotel.io/api/v1/dialer/campaigns/dcmp_abc/dispositions" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "attempt_id": "dcall_xyz",
    "disposition": "callback_later",
    "notes": "Customer asked for tomorrow morning EST",
    "callback_scheduled_at": "2026-05-25T13:00:00-04:00"
  }'
```

```bash cURL theme={null}
# Mark as DNC
curl -X POST "https://api.orbit.devotel.io/api/v1/dialer/campaigns/dcmp_abc/dispositions" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "attempt_id": "dcall_xyz",
    "disposition": "do_not_call",
    "notes": "Customer requested removal from all outreach"
  }'
```

**200 OK**

```json theme={null}
{
  "data": {
    "attempt_id": "dcall_xyz",
    "disposition": "callback_later",
    "status": "callback_scheduled",
    "callback_scheduled_at": "2026-05-25T13:00:00-04:00"
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-05-24T12:00:00Z" }
}
```

**Errors**

| Status | `error.code`               | Cause                                                                                    |
| ------ | -------------------------- | ---------------------------------------------------------------------------------------- |
| `404`  | `NOT_FOUND`                | `attempt_id` not found in this campaign (or cross-tenant).                               |
| `422`  | `VALIDATION_ERROR`         | `callback_scheduled_at` missing on callback disposition, in the past, or >90 days ahead. |
| `503`  | `TENANT_SCHEMA_INCOMPLETE` | The tenant schema was provisioned before migration 280 ran. Contact support.             |

***

## List campaigns

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

**Scope:** `dialer:read`.

Cursor-paginated. Returns campaigns excluding soft-deleted (`status = 'deleted'`) rows.

**Query parameters**

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

```json theme={null}
{
  "data": {
    "items": [
      {
        "id": "dcmp_abc",
        "name": "Q2 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,
        "recording_enabled": true,
        "recording_consent_acknowledged_at": "2026-05-22T10:30:00Z",
        "created_at": "2026-05-22T10:30:00Z"
      }
    ],
    "next_cursor": "MjAyNi0wNS0yMlQxMDozMDowMC4wMDBa",
    "has_more": false
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-05-24T12:00:00Z" }
}
```

***

## Get next contact (preview mode)

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

**Scope:** `dialer:read`.

Agent readiness endpoint for **preview-mode** campaigns. Atomically claims the next eligible contact (CTE + `FOR UPDATE SKIP LOCKED`) so two agents polling concurrently can never receive the same row. For `progressive` / `predictive` campaigns this endpoint returns `204 No Content` — pacing is handled server-side by the scheduler.

**Stale-claim recovery:** A preview contact stuck in `dialing` for >5 minutes (agent abandoned the tab) is automatically re-claimable. Without this, abandoned previews would block the contact forever.

**200 OK**

```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-05-24T12:00:00Z" }
}
```

**204 No Content** — campaign is in `progressive` or `predictive` mode (pacing engine handles dial).

**Errors**

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

***

## Soft-delete a campaign

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

**Scope:** `dialer:write`.

Soft-deletes the campaign. Pending contacts flip to `archived`; terminal contacts (`connected` / `failed` / `dnc` / `exhausted` / `wrong_number`) are left untouched as TCPA-dispute audit trail. All three updates (campaign, lists, contacts) commit in a single transaction.

**Errors**

| Status | `error.code`                | Cause                                                                                                                                                                          |
| ------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `404`  | `NOT_FOUND`                 | Campaign id not found or already soft-deleted.                                                                                                                                 |
| `409`  | `DIALER_CAMPAIGN_IN_FLIGHT` | At least one contact is currently in status `dialing`. Pause the campaign and wait for live legs to terminate, then retry. The `details.in_flight_contacts` count is returned. |

***

## FCC abandon-rate ceiling

`max_abandon_rate` is hard-capped at **0.03 (3%)** per the FCC Telemarketing Sales Rule. Any `POST` or `PATCH` request that sends a value above this is rejected with `422 VALIDATION_ERROR`. The scheduler also auto-flips campaigns to `status: 'aborted'` if the rolling 30-day abandon rate trips the ceiling — `aborted` is terminal and cannot be re-activated; create a new campaign after reviewing the abort reason.

## See also

* [Voice API overview](/api-reference/voice)
* [Compliance — recording consent](/compliance/recording-consent)
* [Voice — emergency calling](/voice/emergency-calling)
