Skip to main content

Campaign fields

Retry configuration

retry_config is normalized on save and echoed back normalized.

The numbers: planned_calls

Each object is one row of a contact list, and its keys are column names. Every column the file upload understands is understood here: market-package columns (zip, state, city), contract-group columns (contract_id, role), timezone, name, and any variable your agent’s flow reads. A nested variables object is sugar and is flattened into the same columns (a top-level key wins over the same key inside variables). Values are converted to text (booleans → true/false, objects → compact JSON), so leading zeros survive.
A phone column is mandatory in every row; a value with fewer than ten digits is rejected as phone_unreadable — the row is rejected, not the request. Row-level issue codes are in The upload report.

Dialing binding

You have no internal trunk or subgroup identifiers, so the platform resolves them: omit trunk_id, route_id and from_number together, and the subgroups bound to your agent are looked up across every trunk visible to your organization. Ambiguity is never resolved by picking the first candidate: dialing from another trunk would spend another caller ID and another quota, and you would find out from the invoice. Read the candidates with:
The response is {"dialing_routes": [{"trunk_id", "trunk_name", "route_id", "route_name", "agents", "caller_numbers"}]} — no trunk connection settings or credentials. An empty agent_id lists every route visible to your organization.
trunk_id is the anchor. Name it and the rest follows the platform’s existing rules: the subgroup is resolved from the trunk and the agent at launch, and the caller ID is chosen by the subgroup itself, following its caller-ID policy. Naming route_id or from_number without trunk_id is refused with 422 dialing_binding_incomplete: the platform will not guess which trunk you meant. A trunk named explicitly but unusable (agent not in its subgroup, empty pool) is not refused on creation — it becomes the binding_invalid blocker.

Attestation

Dialing requires a consent attestation: {"attestation": {"confirmed": true}}. The record is stored immutably against the version of the data it covers and the wording of the campaign’s consent tier, and your API key is the signer — issuing the key is the human act the signature is attributed to (the platform records who issued the key and when). A confirmation of one upload does not carry over to a new one; on POST …/launch the attestation.import_id field is therefore mandatory (→ 422 attestation_import_id_required), and an import_id that is not the campaign’s current import yields the attestation_stale blocker. Batches of added calls are signed separately.

Do-not-call matches: dnc_screen

dnc_screen is the authoritative result. It screens every readable source row before market rejection and deduplication, so matched_rows may be larger than suppressed_contacts. import.dnc_matches is a compatibility projection of items (the same objects plus a row alias of row_number); import.dnc_matched equals matched_rows (0 when the screen is unavailable). Matches block launch with dnc_matches. Sending dnc: "skip" records the same audited decision a human makes in the dashboard. It applies to this exact import_id and fingerprint: a new match or a replacement registry entry makes the decision stale, and launch-check/409 returns the refreshed screen. If every pending contact is matched, all_contacts_suppressed stays unconditional. skip with a clean screen records nothing and the launch proceeds normally. The default dnc: "block" on an explicit launch is recorded as a decision too, and a later block revokes any earlier skip. An explicit launch that omits dnc therefore never rides on a skip somebody approved in the dashboard: pass dnc: "skip" yourself when that is what the call means. A decision needs an upload to apply to: dnc on a launch of a campaign without an import → 409 dnc_decision_without_upload; on an import without a recorded projection → 409 dnc_screen_required. Before the first launch the screen and the matched-only report follow the live registry. The launch operation re-screens immediately before the transition and freezes all matched rows atomically with the import marker, attestation and campaign state. After launch, reads and reports use that snapshot (as_of_launch: true) even when the registry changes. Contacts matched mid-campaign are still suppressed at dial time; progress counts them separately.

Idempotency of campaign requests

POST /v1/campaigns and POST /v1/campaigns/{id}/calls share one protocol:
  • The key is reserved before the work starts, atomically. A second request with the same key while the first is still running gets 409 idempotency_key_in_flight — wait for the first answer instead of retrying again.
  • The body is fingerprinted alongside the key (canonical JSON, key order irrelevant). The same key with a different body is 422 idempotency_key_reuse, never a silent replay of the earlier answer.
  • The same key with the same body replays the stored 201 byte for byte.
  • Keys are scoped to your API key, the endpoint and — for calls — the campaign, and remembered for 24 hours.
  • On creation, a refusal before the campaign exists frees the key; a refusal after it exists keeps it. On calls, any refusal frees the key.