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

# Launch an outbound dialer campaign: pacing, lists, dispositions, callbacks

> Walk through a production-grade outbound dialer campaign — pick a dial mode with safe pacing, prepare a compliant contact list, wire dispositions and wrap-up codes, monitor abandonment, and keep callbacks and DNC honoured.

# Launch an outbound dialer campaign

This guide walks an outbound voice campaign from "here's a list of numbers" to "agents are talking to answered calls without tripping a compliance gate." It ties together the concept pages on [dialer dispositions](/voice/dialer-dispositions) and [wrap-up codes](/voice/wrap-up-codes) into one workable flow, using the dialer API reference ([`/api/v1/dialer`](/api-reference/dialer)) for the exact request shapes.

The sequence below is the one your compliance and QA teams will audit against: pick how you batch, build the contact list with eligibility enforced, choose a dial mode with pacing that cannot over-dial past your abandon ceiling, wire dispositions so the contact's lifecycle status always lands somewhere intentional, and monitor the campaign so abandonment never drifts into FCC territory.

Outbound calls in the dialer egress only through Orbit's wholesale voice path. You never hand them to Telnyx Call Control, DIDWW SMS, or any other provider — the platform enforces that at the dial step.

## 1. Why batch matters — parallel-call limits and the abandonment ceiling

A dialer that just "calls everyone" is a compliance incident waiting to happen. Two numbers bound how fast an outbound batch can go:

* **Parallel-call cap.** In `predictive` mode the pacing scheduler over-dials by the campaign's `pacing_ratio` (defaults 1.2, clamped 0.8–3.0). In `agentless` / voice-agent dispatch the batch is bounded by the campaign's `concurrency_limit` (defaults 10, clamped 1–200). Either way, a single batch can't originate more legs than the cap — keep it to the headcount of agents ready to talk right now, plus the over-dial cushion. Taking a batch bigger than that means answered calls abandon with no agent available.
* **Abandonment ceiling.** The rolling 30-day abandonment rate (`abandon_rate_30d`) drives how hard the pacer dials. The FCC Telemarketing Sales Rule hard-caps abandonment at 3%; the workspace-level ceiling (`GET/PUT /api/v1/voice/dialer-abandon-ceiling`, documented on the [Voice](/api-reference/voice) page) lets you set something stricter. A campaign whose 30-day abandonment trips the ceiling flips `status: 'aborted'` — that status is terminal, you create a new campaign after fixing the pacing settings.

Tune the batch before you launch — don't wait for the abort to trip.

## 2. Prepare the contact list with eligibility enforced

Contacts go into a campaign as a **list**. The dialer API creates one list per upload and de-duplicates by E.164 phone inside the upload; binding the same `contact_id` (from the [Contacts](/api-reference/endpoints/contacts) surface) into the row lets CRM agents deep-link into the contact's record from the dialer UI.

Every contact must clear the eligibility gates **before** the dialer ever touches it. The platform enforces two at pacing-claim time on every tick:

* **Consent.** Only opted-in contacts belong in an outbound dial list. Pull them from a [public consent form](/guides/public-consent-form), an [imports](/guides/campaign-end-to-end) flow, or a CRM tool's exported opt-ins — never a purchased or scraped batch, the messaging-suppression gate will block it anyway.
* **Quiet hours (TCPA).** The resolver asked on every dial tick checks the contact's local timezone and defers the attempt if it's outside the permitted calling window (or the timezone can't resolve, treating it as non-resolvable). The contact comes off the pacing queue with `timezone_blocked` rather than `failed`, and becomes eligible again once the window opens. Never build a list that mixes timezones you can't compute.

The first check is on you; the second is on the scheduler. Ship both.

## 3. Build the campaign — draft, drip, or from a brief

Create the campaign in `draft` status (nothing dials until you activate it) using `POST /api/v1/dialer/campaigns`. If the campaign lives inside a wider engagement flow — say, a voice callback after a two-step SMS-brief drip — the [Campaigns](/api-reference/campaigns) surface provides the surrounding drip or journey template ([`campaigns-drip`](/api-reference/campaigns) / [`journey-templates`](/api-reference/campaigns)) so the dial leg is one step in a larger sequence instead of a standalone blast. If you'd rather write a short brief and let the generator compose the campaign ([`POST /api/v1/campaigns/from-brief`](/api-reference/campaigns)), that works too — the output is the same draft shape either way.

Attach caller ID, pick a dial mode (step 4 next), and decide what happens when a contact answers. The `campaign_script` field carries the guided-dialog script your agents see; omitted, the dialer stores the canonical empty script `{ "version": 1, "sections": [], "branches": {} }`.

## 4. Choose the dial mode — preview, progressive, predictive, agentless

| Mode          | Who talks to the answered call                    | Pacing behaviour                                                                                                                                                                 |
| ------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `preview`     | Human agent, after inspecting the claimed contact | Agent polls `GET /api/v1/dialer/next-call` to claim a contact, then dials with `POST /api/v1/dialer/dial`. A claimed row stale for >5 minutes drops back into the eligible pool. |
| `progressive` | Human agent                                       | One live dial per ready agent — the pacing engine rings the next eligible contact whenever an agent signals the queue.                                                           |
| `predictive`  | Human agent                                       | The pacing engine over-dials by `pacing_ratio` per ready agent, bounded by the abandon ceiling.                                                                                  |
| `agentless`   | Nobody — plays an audio URL and hangs up          | Server-paced broadcast, the `broadcast_message_url` is required.                                                                                                                 |

Pick `preview` or `progressive` when the call is high-stakes (the agent reviews the CRM record first). Pick `predictive` only when the abandon ceiling has room — the pacing engine will push toward it.

**Pacing safety settings.** In `predictive` mode the knobs are `pacing_ratio` (0.8–3.0) and `max_abandon_rate` (default 0.03, clamped ≤ 0.03). Either side of that, two opt-in feedback controllers govern how hard the executor sends: the campaign can flip on `variables.adaptive_pacing` so rising failure / spam-block rates throttle the batch instead of hammering the carrier. It's off by default; when it's on it clamps the effective send rate between a healthy floor and the min pace factor. Wire this through `campaign.variables.adaptive_pacing` — no schema change needed.

## 5. Wire dispositions and wrap-up codes into the flow

Every answered-or-completed attempt has to land a disposition — otherwise the contact status is stuck `pending` and the return-eligibility guard reclaims it. The agent posts to `POST /api/v1/dialer/campaigns/{id}/dispositions` with the attempt id and a string disposition, and the matrix on the [dialer dispositions](/voice/dialer-dispositions) page maps that string to a contact-status. Crucial: a freeform string that doesn't match a known disposition routes to `failed` — deliberately, so a new button in the softphone can't silently land every contact in `connected`.

Two special-case dispositions:

* **`callback_later` / `callback`.** Routes the contact to `callback_scheduled` and stamps `callback_scheduled_at` as the next attempt moment. The pacing scheduler re-picks the contact at that ISO moment. A `callback_in_queue_requests` row takes over when the callback comes from an inbound queue (`callback-in-queue` controller) instead — that backs virtual hold.
* **`dnc` / `do_not_call`.** Routes the contact to `dnc` and INSERTs the phone into the tenant-wide do-not-call list. Any future pacing tick across any campaign in the workspace blocks at the DNC gate. This is the fix for the callback DNC-bypass pattern we caught in May — the suppression check happens on the pacing tick, not just at the disposition, so an agent's disposition and a callback scheduling both respect the list.

For an inbound queue that routes a human agent through wrap-up, see [wrap-up codes](/voice/wrap-up-codes): the queue carries its own catalog and can require one before the agent returns to `available`. The dialer disposition matrix doesn't replace that queue catalog — a preview-mode agent and an inbound queue agent use the appropriate one.

## 6. Monitor — live stats and the abandonment rollup

The dialer API returns a full live-stats row per campaign (`GET /api/v1/dialer/campaigns/{id}/stats`) with per-status contact breakdown and a rolling-30-minute abandonment rate that mirrors what the pacing engine throttles against. Watch `abandon_rate_30m` against `max_abandon_rate`: if the former creeps toward the latter you've got more answered calls than ready agents; slow the batch.

For callbacks that arrive through the in-queue virtual-hold surface, the `GET /api/v1/voice/callback-in-queue` endpoint (external token) returns the pending requests your queue saved; successful dispatch flips them to `dispatched`, a cancellation `DELETE`s them, and the report endpoint shows the policy's hit rate.

## 7. Comply — every gate, never bypassed

Compliance on a dialer campaign is not one feature, it's a stack. The dialer enforces all of these on every pacing tick:

* **Do-not-call.** Tenant do-not-call list, checked at claim time. See `dnc` disposition above.
* **Quiet hours.** Time-zone check at claim time. Contacts `timezone_blocked` if the window is closed, become eligible when it opens.
* **Abandon ceiling.** The 30-day rolling rate flips the campaign `aborted` if it trips `max_abandon_rate`; the FCC cap can't be exceeded.
* **Recording.** Setting `recording_enabled: true` requires `recording_consent_acknowledged: true` in the same request attributed to an operator (never an API-key caller). See [Recording Consent](/compliance/recording-consent).

Treat this stack as comprehensive — if a gate rejects a dial, the response says which one and the platform won't re-issue it until you clear the same gate.

## 8. Post-call AI scoring — dialer parity

Once **post-call synthesis** is enabled under **Settings → Voice**, every completed outbound dialer call with a transcript is scored the same way your inbound calls already are. As soon as the call ends, the synthesis pipeline classifies it on three axes and writes the result back onto the call:

* **Sentiment label** — `positive`, `neutral`, `negative`, or `frustrated`.
* **Customer intent** — `purchase_inquiry`, `support`, `cancel`, or `other`.
* **Resolution outcome** — `resolved`, `escalated`, or `incomplete`.

You get the same recall-without-replaying-recordings benefit that inbound traffic has: coach the agent, filter the campaign's completed calls by sentiment or outcome, and never read a transcript to know how the call went. If the transcript has no decipherable speech — a dead-air test call, a voicemail drop — the call scores deterministically as `neutral` / `other` / `incomplete` rather than sticking as unscored.

### Where the score lives

* **On the call.** The score is persisted onto the call record and rendered on the call detail page in the dashboard — open the call from the voice Calls list and the sentiment, intent, and outcome show alongside the summary and key points.
* **In the `call.synthesized` webhook.** The same score ships to every webhook endpoint you subscribe to the event, with a `direction` marker (`"inbound"` or `"outbound"`) in the payload so your reporting splits inbound from outbound scoring. That split is what lets a campaign report count only dialer calls.

### Example payload

```json theme={null}
{
  "event": "call.synthesized",
  "call_id": "call_9f2c4b",
  "direction": "outbound",
  "from": "+12025551234",
  "to": "+14155550107",
  "duration_seconds": 214,
  "ended_at": "2026-08-26T14:03:41.000Z",
  "synthesis": {
    "summary": "Customer asked about the annual plan and agreed to a follow-up next week.",
    "sentiment": "positive",
    "customer_intent": "purchase_inquiry",
    "resolution": "resolved",
    "key_points": ["Asked about annual pricing", "Wants a follow-up call"],
    "follow_up_actions": ["Schedule follow-up next week"]
  },
  "synthesised_at": "2026-08-26T14:04:02.000Z"
}
```

The `direction` enum is exactly `"inbound"` or `"outbound"` — an outbound dialer call always carries `"outbound"`, so filtering on it in your webhook consumer cleanly separates the two sides of the business.

### Off by default — the synthesis toggle gates it

Scoring only runs when post-call synthesis is enabled (Settings → Voice — the same toggle that governs inbound scoring and the webhook emission). With the toggle off, no synthesis runs and no scoring happens for either direction. If the workspace's PII-redaction setting is on, transcripts are redacted before analysis; the score then reflects the redacted transcript. Turn the toggle on and every completed dialer call starts scoring from the next tick — no campaign change needed.

## Worked example — launch a test list of 25 contacts

```bash theme={null}
export ORBIT_API_KEY="dv_live_sk_your_key_here"
export CAMPAIGN_ID=""

# 1. Create the campaign (draft — pacing knobs stay off until activate)
CAMPAIGN=$(curl -s -X POST "https://api.orbit.devotel.io/api/v1/dialer/campaigns" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sales outbound — August test batch",
    "mode": "predictive",
    "caller_id_e164": "+12025551234",
    "pacing_ratio": 1.2,
    "max_abandon_rate": 0.02,
    "target_service_level_seconds": 20
  }')
CAMPAIGN_ID=$(echo "$CAMPAIGN" | jq -r '.data.id')
echo "campaign: $CAMPAIGN_ID"

# 2. Upload the test list — 25 contacts (replace phones with yours)
CONTACTS='[
  {"phone_e164": "+14155550101", "display_name": "Amy Lee"},
  {"phone_e164": "+14155550102", "display_name": "Brian Cole"},
  {"phone_e164": "+14155550103"}, {"phone_e164": "+14155550104"},
  {"phone_e164": "+14155550105"}, {"phone_e164": "+14155550106"},
  {"phone_e164": "+14155550107"}, {"phone_e164": "+14155550108"},
  {"phone_e164": "+14155550109"}, {"phone_e164": "+14155550110"},
  {"phone_e164": "+14155550111"}, {"phone_e164": "+14155550112"},
  {"phone_e164": "+14155550113"}, {"phone_e164": "+14155550114"},
  {"phone_e164": "+14155550115"}, {"phone_e164": "+14155550116"},
  {"phone_e164": "+14155550117"}, {"phone_e164": "+14155550118"},
  {"phone_e164": "+14155550119"}, {"phone_e164": "+14155550120"},
  {"phone_e164": "+14155550121"}, {"phone_e164": "+14155550122"},
  {"phone_e164": "+14155550123"}, {"phone_e164": "+14155550124"},
  {"phone_e164": "+14155550125"}
]'
curl -s -X POST "https://api.orbit.devotel.io/api/v1/dialer/campaigns/$CAMPAIGN_ID/lists" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"name\": \"test batch 25\", \"contacts\": $CONTACTS, \"max_attempts_per_contact\": 3}" | jq

# 3. (Optional) tighten the org abandon ceiling to 2% for this workspace
curl -s -X PUT "https://api.orbit.devotel.io/api/v1/voice/dialer-abandon-ceiling" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true, "ceiling_pct": 2.0}' | jq

# 4. Activate the campaign — pacing begins
curl -s -X PATCH "https://api.orbit.devotel.io/api/v1/dialer/campaigns/$CAMPAIGN_ID" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}' | jq '.data.status'
```

Once `active`, the pacing engine starts claiming eligible contacts; an agent polls `GET /api/v1/dialer/next-call?campaign_id=$CAMPAIGN_ID` (preview) or the scheduler auto-dials (predictive/advanced), handles the call, and posts a disposition:

```bash theme={null}
curl -s -X POST "https://api.orbit.devotel.io/api/v1/dialer/campaigns/$CAMPAIGN_ID/dispositions" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"attempt_id": "<attempt_id>", "disposition": "sale", "notes": "closed"}' | jq
```

Watch `GET /api/v1/dialer/campaigns/$CAMPAIGN_ID/stats` for the 30-minute abandonment rollup; pause with `PATCH {"status": "paused"}` and re-check the batch size when it drifts.

## See also

* [Dialer API](/api-reference/dialer) — full request/response schemas for every endpoint used above
* [Dialer dispositions](/voice/dialer-dispositions) — the mapping from disposition string to contact lifecycle status
* [Wrap-up codes](/voice/wrap-up-codes) — the per-queue code catalog enforced before an agent returns to available
* [Recording consent](/compliance/recording-consent) — required before a campaign with `recording_enabled: true` goes live
* [Campaign A/B testing](/guides/campaign-ab-testing) — run variants if the campaign is part of a wider journey
* [Conversation intelligence](/concepts/conversation-intelligence) — how post-call synthesis, sentiment, and outcomes apply across inbound and outbound calls
