Campaigns & scrub
A campaign never launches on an unexamined list. The path is: import → scrub → acknowledge the scrub report → create → launch. Each arrow is a gate with a named refusal.
Import contacts
Section titled “Import contacts”POST /v1/contact-imports (multipart CSV + column-mapping JSON) returns
202 with an import job. Poll GET /v1/contact-imports/{id} to a terminal
status, because imports are not in the webhook catalog, so polling is the
completion signal. Per-row failures (bad E.164, duplicate, missing required
column) are reported at GET /v1/contact-imports/{id}/errors as JSON or CSV
(Accept: text/csv). Rows land in a contact list (cl_…).
Provenance: two tiers, and only one of them is a refusal
Section titled “Provenance: two tiers, and only one of them is a refusal”Scraped contacts - and anything else obtained by unauthorized extraction - are refused on the platform, however they arrive: upload, API, or CRM sync. No supplier agreement stands behind data that was taken, so there is nothing for an attestation to attest to.
Lists you purchased, rented, or had appended are a different tier. They are your data, acquired under your own supplier agreement, dialed from your own carrier account, so you may import and call them once your account carries the executed Cold Outreach & Data Provenance Waiver (published with the rest of the set at www.vocapable.com/legal). The same waiver covers outreach to people who have not consented to be called. You sign it, and it places the duty for that program on you: the platform’s controls are operational safeguards, and none of them is a determination that a given call is permitted.
Which tier you are in is not a field on the import. A contact list’s
source describes how the rows arrived (upload or api) and never where
they came from; there is no purchased value to send, and no endpoint asks
the question at import time. It is answered once on your account’s onboarding
screen, and the waiver is what carries the answer. Every import still sends
consent_attestation (signer name, signer title, affirmation, and one
consent_basis of pewc, pec, ebr, or transactional), which the list
records permanently: PATCH /v1/contact-lists/{id} renames a list and
deliberately cannot rewrite its basis or attestation ref. That basis is not
paperwork. It is read on every attempt, and what it says decides whether the
dial happens - see Dial time.
Scrub and acknowledge
Section titled “Scrub and acknowledge”POST /v1/scrub-runs {"target": {"contact_list_id": "cl_…"}} → 202; poll
the run until completed. The report carries totals
(contacts/dialable/blocked), a blocked_breakdown by reason (federal DNC,
internal DNC, reassigned, litigator, state-blocked, line type), the
dataset_versions it scrubbed against, and an expires_at.
Scrub stages fail closed, with one documented exception: if the
litigator-scrub vendor is down, the run completes flagged
"risk_flags": ["litigator_unscreened"] rather than blocking.
A human or system must then acknowledge the report with
POST /v1/scrub-runs/{id}/acknowledge, recording actor, timestamp, and the
totals as seen. Acknowledging totals that do not match the run refuses with
scrub_totals_mismatch: the acknowledgment
attests to what you saw. Runs go stale at expires_at
(scrub_run_expired).
Create the campaign
Section titled “Create the campaign”POST /v1/campaigns names the published agent version, the target list, a
number pool, a compliance profile, the schedule, retry policy, and the
scrub_run_id. Caller ID is always a pool reference, and the API never
accepts an inline array of numbers; rotation order and per-number daily caps
are properties of the pool. Calling windows are evaluated in each contact’s
timezone, and the gate may apply stricter policy restrictions for that contact.
The current portal and API expose no calling-policy configuration.
telephony_account_id defaults to your account; when supplied it must
be the account behind every number in the pool.
Launch, and every way it refuses
Section titled “Launch, and every way it refuses”POST /v1/campaigns/{id}/launch (with Idempotency-Key, required) returns
200 {"status": "running", "dialable_contacts": …}. The gate refuses with a
409 problem when:
scrub_requiredmeans no scrub run covers the target, or it has not finishedscrub_unacknowledgedmeans the report is fresh but nobody acknowledged itscrub_expiredmeans the run went stale; re-run itagent_not_publishedmeans drafts are never dialedtelephony_account_unverifiedmeans an unverified carrier account cannot dialspend_cap_exceeded/spend_state_unknownmeans the spend cap is spent, or cannot be measured at all; both fail closed
At dial time each attempt still passes the ordered pacer governors, abandon-rate ceiling, policy re-check, per-account CPS budget, spend cap, caller-ID rotation caps. AMD-detected machines are hung up and recorded as attempts only.
Dial time reads the consent basis you sent
Section titled “Dial time reads the consent basis you sent”The waiver decides what your account may run. It does not change what the gate computes for an individual contact, and integrators are caught by that distinction more than by anything else on this page.
Every attempt resolves the campaign purpose to a required consent class and compares it against the basis on record for that contact:
- a basis below the requirement refuses with
consent_insufficient- this is also what an expired consent record produces, because an expired basis is held as none - a basis high enough but scoped to another purpose refuses with
consent_scope_mismatch; consent to appointment reminders does not authorize a sales campaign
Both are policy refusals rather than scrub blocks, which is a distinction
your reconciliation code can see: they land at
GET /v1/campaigns/{id}/outcomes as disposition: "blocked_by_policy",
where a contact stopped at materialization lands as scrub_blocked. On the
ad-hoc path the same reasons ride the errors[] array of the 409
contact_not_dialable problem. Accepting the
waiver clears neither, and neither is a defect: a cold program still has to
describe the basis it holds, and pewc, pec, ebr, and transactional
remain the only four values the API accepts.
The floor controls are in the same position. The AI-disclosure opener, internal-DNC suppression at batch and at dial time, and opt-out capture run on cold outreach exactly as on consented traffic; no agreement you sign and no field you send reaches them.
Monitor and reconcile
Section titled “Monitor and reconcile”Poll GET /v1/campaigns/{id}/stats at 5 seconds (it carries an ETag, so
unchanged polls are 304), follow row-level activity at
GET /v1/campaigns/{id}/attempts?updated_at.gte=…, and reconcile one row
per targeted contact at GET /v1/campaigns/{id}/outcomes, including
contacts blocked at materialization (disposition: "scrub_blocked"). Bulk
extraction is POST /v1/exports. Outcome pushes ride
webhooks.