Skip to main content

Managing a campaign

Statuses. draft → (launch) → scheduled (when starts_at is in the future) or running; scheduled becomes running when the window opens; runningpaused; runningcompleted 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:
The rule across the whole campaign surface: an action that changed nothing is always an error; a 2xx means the state in the body is the new state.

Adding numbers to an existing campaign

The campaign owns the agent, the dialing binding, the retry rules and the working hours; the batch carries numbers, variables and a signature only. Each element has the shape of one 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_required422 calls_required (empty calls) → 422 calls_batch_too_large422 attestation_invalid404 campaign_not_found409 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.
The batch is accepted whole or refused whole. One row with a data error refuses the entire batch with 422 calls_rejected; one number on the do-not-call registry refuses it with 422 calls_dnc_matched — a distinct code, so your system can tell “fix the rows” from “remove these people from the list”. When a batch has both kinds of problem, calls_rejected wins. Structural refusals of the parser (no phone column, no usable rows, …) are calls_rejected too. No contacts are created for a refused batch, and no signature is recorded.
Both refusals carry error.import:
Here 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_large above it, 422 calls_required for an empty batch. One request counts once against the per-key rate limit.
  • Signatureattestation must be exactly {"confirmed": true}: no other keys, no import_id (422 attestation_invalid otherwise). 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: true under the Kazakhstan market profile, a row without a timezone is 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 and planned_calls on creation keep the fallback.
  • Duplicates — inside a batch duplicate_action applies 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 default retry_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_screen of GET /v1/campaigns/{id} keep pointing at the import the campaign was launched with. The batch’s own screen is in the 201 body and at GET /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_calls at all and starts running empty (no no_pending_contacts blocker);
  • is closed only by ends_at arriving or by cancel, never by a drained queue.
The flag is set at creation and cannot be changed later; it is returned in the campaign object and in the list.

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 to suppressed with the reason withdrawn_by_client and is never dialed again in that campaign. For an awaiting contact the call has already happened: the withdrawal cancels the redial the verdict might have scheduled.
  • What is not. not_withdrawable names the reason: in_call — the call is in progress or the contact was just taken for dialing (the withdrawal does not hang up; use POST /v1/calls/{call_id}/end, and withdraw again after the call if a redial must not happen), done, exhausted, or already_suppressed — excluded earlier by the platform itself (do-not-call, a frequency cap, geography); that earlier reason is kept.
  • Repeating is safe. No Idempotency-Key is needed: a contact you withdrew before comes back in already_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_id of 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: invalid returns what could not be read (not a number, no digits, an empty id), not_found what 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.