Managing a campaign
Statuses.
draft → (launch) → scheduled (when starts_at is in the future) or running; scheduled becomes running when the window opens; running ⇄ paused; running → completed when the queue drains (nothing pending and nothing in progress) — except for a campaign with accepts_call_additions: true, which completes only when ends_at passes; any non-final status → cancelled. completed and cancelled are final. Reaching ends_at closes every campaign.
On resume the blockers no_pending_contacts, attestation_required, attestation_stale, contacts_geo_rejected and dnc_screen_required are not applied.
The pause / resume / cancel responses carry the campaign object without blockers and warnings: those paths do not run the launch checks, and an empty array would read as “checked and clean”. Use launch-check when you need them.
Repeating a transition is not an error. A campaign already in the target state answers 200 with its current object — a retry after a network timeout is not a second intention. A transition impossible from the current state (resuming a cancelled campaign, pausing a draft) is 409 campaign_transition_invalid. The difference between “already done” and “cannot be done” is the one you need.
A blocked launch or resume is an error, not a 200. Creation answers 201 because a campaign and a report really were created; a launch — or a resume, which re-runs the same checks — that starts nothing answers 409 launch_blocked, and the reasons travel in the same response:
2xx means the state in the body is the new state.
Adding numbers to an existing campaign
planned_calls row (Campaign fields) — its keys are column names. Unknown top-level keys are ignored: an agent_id in the body does not change the campaign’s agent, and a dnc key does not bypass the registry.
A batch is accepted into any campaign that is not completed or cancelled — that is, into draft, scheduled, running and paused, whether or not it is open to call additions. A closed one answers 409 campaign_closed.
Order of refusals, all before any row is parsed: 422 idempotency_key_required → 422 calls_required (empty calls) → 422 calls_batch_too_large → 422 attestation_invalid → 404 campaign_not_found → 409 campaign_closed → idempotency (422 idempotency_key_reuse / 409 idempotency_key_in_flight / replay). Then the rows are parsed: 409 market_package_not_installed, 422 contact_rows_rejected, 502 contact_file_storage_unavailable as on creation, and the two batch refusals below.
Both refusals carry error.import:
issues holds the first 50 issues; the full per-row report is GET /v1/imports/{import_id}/report (and ?only=dnc_match for the registry matches). A refused batch is kept as an import record with status: "refused" for exactly that purpose.
Success is 201:
campaign is the short view (the same object as a list item). blockers means “accepted, but not dialing right now, and why”: the same launch checks as launch-check minus the ones a fresh batch makes moot (no_pending_contacts, attestation_required, dnc_screen_required, all_contacts_suppressed, dnc_matches), plus campaign_paused. Items on this path carry code and field only.
Rules worth knowing before you stream numbers from a CRM:
- Cap — up to 1000 rows per request by default (a deployment setting):
422 calls_batch_too_largeabove it,422 calls_requiredfor an empty batch. One request counts once against the per-key rate limit. - Signature —
attestationmust be exactly{"confirmed": true}: no other keys, noimport_id(422 attestation_invalidotherwise). Every accepted batch is signed separately and bound to its own upload; the launch signature is not reused for rows that did not exist at launch. - Time zone — in a campaign with
use_contact_timezone: trueunder the Kazakhstan market profile, a row without atimezoneis a data error (timezone_required) and refuses the batch: the platform will not fall back to the campaign zone for a row nobody looked at. File uploads andplanned_callson creation keep the fallback. - Duplicates — inside a batch
duplicate_actionapplies as it does for a file. A number that already has a contact in the campaign — even one already dialed — is accepted again as a new contact and dialed again: for an adapter that manages its own redials, a repeat is the redial. If your system manages redials, keep the defaultretry_config.attempts = 1; with a larger budget redials become two-level. - The batch never becomes the campaign’s launch import — the frozen do-not-call snapshot, the launch signature and the
dnc_screenofGET /v1/campaigns/{id}keep pointing at the import the campaign was launched with. The batch’s own screen is in the201body and atGET /v1/imports/{import_id}. - Idempotency — the key is scoped to the campaign, so the same key and body sent to two campaigns are two batches. A refused batch frees the key: fix the rows and resend with the same key (a second refused import record is created — diagnostically useful, no calls involved). A repeat of an accepted batch replays the answer; the same key with a different body is
422 idempotency_key_reuse.
Campaigns open to added numbers
An ordinary campaign completes itself as soon as its queue drains — and a completed campaign accepts no calls. To keep a campaign waiting for numbers, create it with"accepts_call_additions": true. Such a campaign:
- requires
ends_at(422 campaign_open_ends_at_required), no further than the operator’s horizon — 30 days by default (422 campaign_open_ends_at_too_far); - may be created with no
planned_callsat all and starts running empty (nono_pending_contactsblocker); - is closed only by
ends_atarriving or bycancel, never by a drained queue.
Progress and contact states
GET /v1/campaigns/{id}/progress:
by_state uses the platform’s own state names — there is no translation layer, because the difference between “reached” and “out of attempts” matters to you. States with a zero count are omitted.
ready is the number of pending contacts dialable right now; holds counts pending contacts held back (reasons in held_reasons); suppressed_reasons breaks down the suppressed state; dnc_suppressed_at_launch + dnc_suppressed_mid_campaign = suppressed_reasons.dnc_suppressed; frequency reports contacts currently held or blocked by the per-number frequency caps and the caps governing them. Reason keys inside held_reasons, suppressed_reasons and by_governing_cap are machine codes and may grow.
An ordinary campaign closes itself when its queue drains — a campaign created with
accepts_call_additions: true does not (only ends_at or cancel closes that one). There is no “campaign finished” webhook either way — poll progress or GET /v1/campaigns/{id}.Withdrawing a contact from dialing
Stop dialing a number before the call happens: the debt was paid, the number belongs to someone else, the case was settled in another channel.200 OK lists the contacts by outcome; all five keys are always present:
- What is withdrawn. A contact waiting to be dialed (
pending, a scheduled redial included) or waiting for the analytics verdict (awaiting). It moves tosuppressedwith the reasonwithdrawn_by_clientand is never dialed again in that campaign. For anawaitingcontact the call has already happened: the withdrawal cancels the redial the verdict might have scheduled. - What is not.
not_withdrawablenames the reason:in_call— the call is in progress or the contact was just taken for dialing (the withdrawal does not hang up; usePOST /v1/calls/{call_id}/end, and withdraw again after the call if a redial must not happen),done,exhausted, oralready_suppressed— excluded earlier by the platform itself (do-not-call, a frequency cap, geography); that earlier reason is kept. - Repeating is safe. No
Idempotency-Keyis needed: a contact you withdrew before comes back inalready_withdrawn. - Addressing. A number matches by its last ten digits, so any spelling works, and a number may be sent as a string or an integer. One number reaches every contact that carries it — in a campaign with duplicates, all of them. A contact id is the
metadata.contact_idof the call webhooks; a contact that was never dialed is addressed by its number. - The organization endpoint reaches dashboard campaigns too: every live campaign of your organization, however it was created. Drafts and closed campaigns are not touched.
- Partial processing. One bad address does not fail the request:
invalidreturns what could not be read (not a number, no digits, an empty id),not_foundwhat found no contact — both exactly as you sent them. At most 1000 addresses per request by default. - Consequences. Withdrawing the last waiting contacts of a running campaign completes it on the next dialer pass, unless the campaign is open to added numbers. A withdrawn number returns to dialing only with a new batch of calls.
There is no withdrawal screen in the dashboard yet: the
withdrawn_by_client reason shows up in the campaign progress breakdown, but the operation itself is available only through the API.