Campaigns
Generated from
DialerDigital/corecommit6e9f026f0a72— specopenapi/dialer-v1.yamlv0.36.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
Campaign CRUD, lifecycle, manual dial, rewind.
Endpoints
List campaigns
GET /v1/campaignsResponses
200
All campaigns of the tenant.
| Field | Type | Required | Description |
|---|---|---|---|
campaigns | array of Campaign | yes | |
campaigns[].id | string (uuid) | no | |
campaigns[].tenant_id | string (uuid) | no | |
campaigns[].name | string | no | |
campaigns[].state | draft · running · paused · completed · archived · suspended_balance | no | |
campaigns[].campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
campaigns[].channel | voice · sms | no | Derived from the type profile — which runner the lifecycle dispatches to. |
campaigns[].sms_body | string | null | no | |
campaigns[].abandonment_threshold | string | null | no | Decimal rendered as a JSON string. |
campaigns[].min_dial_ratio | string | null | no | |
campaigns[].max_dial_ratio | string | null | no | |
campaigns[].calling_window_start | string | null | no | HH:MM:SS. |
campaigns[].calling_window_end | string | null | no | |
campaigns[].default_timezone | string | null | no | IANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false. |
campaigns[].calling_window_destination_local | boolean | no | Read calling_window_start/_end in the DESTINATION's local hour instead of default_timezone. false by default, which is the behaviour every campaign had before this field existed. When true the window must be open in EVERY timezone the destination could be in, and a destination whose zone cannot be resolved does not open it. This is a tenant-defined bound only: it can narrow the LEGAL quiet-hours window, never widen it. |
campaigns[].record_calls | boolean | no | |
campaigns[].ai_voice | boolean | no | |
campaigns[].amd_enabled | boolean | null | no | The STORED per-campaign Answering Machine Detection toggle. null = inherit the type profile's default (on for blaster/TTS/IVR/AI types, off for agent types). |
campaigns[].optout_digit | string | null | no | The single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key. |
campaigns[].tts_prompt | string | null | no | The operator's script for the tts_* treatments; null = no prompt. |
campaigns[].ivr_menu | object | no | The IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu. |
campaigns[].deferred_bridge | boolean | no | |
campaigns[].consult_config | object | no | The consult-line config as written ({} = disabled). See CampaignWrite.consult_config. |
campaigns[].carrier | string | null | no | |
campaigns[].caller_ids | array of string | no | Prior round-robin pool of E.164 strings (used when caller_id_pool is empty). |
campaigns[].caller_id_pool | array of string (uuid) | no | DID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed). |
campaigns[].created_at | string (date-time) | no | |
campaigns[].updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}Create a campaign (state draft)
POST /v1/campaignscampaign_type selects the pacing profile (predictive default; preview/manual are the human-initiated modes POST /v1/campaigns/{id}/dial requires; sms_blast is the :sms-channel type). sms_body is REQUIRED for sms_blast and must contain the STOP opt-out notice (Reg F §1006.6(e)); it supports {{debt_ref}}, {{consumer_ref}}, {{account_number}}, {{amount}}, {{state}} merge vars. Every id in caller_id_pool must exist in the tenant's /v1/dids registry (foreign ids are indistinguishable from unknown → 400 invalid). Duplicate name → 409 conflict.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | |
campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
sms_body | string | no | REQUIRED for sms_blast; must contain the STOP opt-out notice; supports merge vars. |
abandonment_threshold | number | no | |
min_dial_ratio | number | no | |
max_dial_ratio | number | no | |
calling_window_start | string | no | HH:MM:SS. |
calling_window_end | string | no | |
default_timezone | string | no | |
calling_window_destination_local | boolean | no | |
record_calls | boolean | no | |
ai_voice | boolean | no | |
amd_enabled | boolean | null | no | Per-campaign Answering Machine Detection toggle; null (the default) inherits the type profile. Whether a detected machine gets a voicemail drop or a hang-up is still the compliance gates' decision, not this flag's. |
optout_digit | string | no | Exactly ONE DTMF key: pressing it adds the number to the tenant DNC and ends the call. Anything else is 400 invalid naming the field. "" clears it. |
tts_prompt | string | no | Script rendered to audio for the tts_blast/tts_ivr treatments (one render per distinct text). Capped at 2000 characters — several minutes of speech — so the renderer is never handed an unbounded document. "" clears it. |
ivr_menu | object | no | Digit → action map dispatched on a keypress, flat ({"1": "transfer_agent", "9": "optout"}) or nested ({"prompt": "...", "entries": {"1": {"action": "hangup"}, "2": {"submenu": {...}}}}). Keys are single DTMF keys; actions are transfer_agent, optout, hangup; depth ≤ 5, ≤ 64 nodes. A menu that does not validate is 400 invalid with the engine's reason (Dialer.IVR.Menu.validate/1) — it is never stored. {} = no menu. |
deferred_bridge | boolean | no | Agent campaigns: park the consumer on answer and bridge to the agent only after a HUMAN AMD verdict. Effective only while AMD is on for the campaign. |
consult_config | object | no | Consult-line IVR config, CLOSED schema: factor (one of account_last4, ssn_last4, dob, pin, zip), optional max_attempts (1..10), optional expose (subset of balance, account_status, minimum_payment, due_date, last4_account), optional factor_opts (length positive integer, dob_format mmdd|mmddyyyy). Any other key is 400 invalid naming it. {} = disabled. |
carrier | string | no | |
caller_ids | array of string | no | |
caller_id_pool | array of string (uuid) | no |
{
"name": "postman-golden-1751600000",
"default_timezone": "America/Chicago",
"calling_window_start": "00:00:00",
"calling_window_end": "23:59:59",
"abandonment_threshold": 0.03,
"caller_ids": [
"+13125550199"
]
}Responses
201
Created (state draft).
| Field | Type | Required | Description |
|---|---|---|---|
campaign | object (Campaign) | yes | |
campaign.id | string (uuid) | no | |
campaign.tenant_id | string (uuid) | no | |
campaign.name | string | no | |
campaign.state | draft · running · paused · completed · archived · suspended_balance | no | |
campaign.campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
campaign.channel | voice · sms | no | Derived from the type profile — which runner the lifecycle dispatches to. |
campaign.sms_body | string | null | no | |
campaign.abandonment_threshold | string | null | no | Decimal rendered as a JSON string. |
campaign.min_dial_ratio | string | null | no | |
campaign.max_dial_ratio | string | null | no | |
campaign.calling_window_start | string | null | no | HH:MM:SS. |
campaign.calling_window_end | string | null | no | |
campaign.default_timezone | string | null | no | IANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false. |
campaign.calling_window_destination_local | boolean | no | Read calling_window_start/_end in the DESTINATION's local hour instead of default_timezone. false by default, which is the behaviour every campaign had before this field existed. When true the window must be open in EVERY timezone the destination could be in, and a destination whose zone cannot be resolved does not open it. This is a tenant-defined bound only: it can narrow the LEGAL quiet-hours window, never widen it. |
campaign.record_calls | boolean | no | |
campaign.ai_voice | boolean | no | |
campaign.amd_enabled | boolean | null | no | The STORED per-campaign Answering Machine Detection toggle. null = inherit the type profile's default (on for blaster/TTS/IVR/AI types, off for agent types). |
campaign.optout_digit | string | null | no | The single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key. |
campaign.tts_prompt | string | null | no | The operator's script for the tts_* treatments; null = no prompt. |
campaign.ivr_menu | object | no | The IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu. |
campaign.deferred_bridge | boolean | no | |
campaign.consult_config | object | no | The consult-line config as written ({} = disabled). See CampaignWrite.consult_config. |
campaign.carrier | string | null | no | |
campaign.caller_ids | array of string | no | Prior round-robin pool of E.164 strings (used when caller_id_pool is empty). |
campaign.caller_id_pool | array of string (uuid) | no | DID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed). |
campaign.created_at | string (date-time) | no | |
campaign.updated_at | string (date-time) | no |
{
"campaign": {
"id": "3f2c1b0a-9d8e-4c7b-a6f5-4e3d2c1b0a99",
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"name": "postman-golden-1751600000",
"state": "draft",
"campaign_type": "predictive",
"channel": "voice",
"sms_body": null,
"abandonment_threshold": "0.03",
"min_dial_ratio": "1.0",
"max_dial_ratio": "3.0",
"calling_window_start": "00:00:00",
"calling_window_end": "23:59:59",
"default_timezone": "America/Chicago",
"record_calls": true,
"ai_voice": false,
"carrier": null,
"caller_ids": [
"+13125550199"
],
"caller_id_pool": [],
"created_at": "2026-07-05T10:00:00Z",
"updated_at": "2026-07-05T10:00:00Z"
}
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Fetch a campaign
GET /v1/campaigns/{id}Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The campaign.
| Field | Type | Required | Description |
|---|---|---|---|
campaign | object (Campaign) | yes | |
campaign.id | string (uuid) | no | |
campaign.tenant_id | string (uuid) | no | |
campaign.name | string | no | |
campaign.state | draft · running · paused · completed · archived · suspended_balance | no | |
campaign.campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
campaign.channel | voice · sms | no | Derived from the type profile — which runner the lifecycle dispatches to. |
campaign.sms_body | string | null | no | |
campaign.abandonment_threshold | string | null | no | Decimal rendered as a JSON string. |
campaign.min_dial_ratio | string | null | no | |
campaign.max_dial_ratio | string | null | no | |
campaign.calling_window_start | string | null | no | HH:MM:SS. |
campaign.calling_window_end | string | null | no | |
campaign.default_timezone | string | null | no | IANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false. |
campaign.calling_window_destination_local | boolean | no | Read calling_window_start/_end in the DESTINATION's local hour instead of default_timezone. false by default, which is the behaviour every campaign had before this field existed. When true the window must be open in EVERY timezone the destination could be in, and a destination whose zone cannot be resolved does not open it. This is a tenant-defined bound only: it can narrow the LEGAL quiet-hours window, never widen it. |
campaign.record_calls | boolean | no | |
campaign.ai_voice | boolean | no | |
campaign.amd_enabled | boolean | null | no | The STORED per-campaign Answering Machine Detection toggle. null = inherit the type profile's default (on for blaster/TTS/IVR/AI types, off for agent types). |
campaign.optout_digit | string | null | no | The single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key. |
campaign.tts_prompt | string | null | no | The operator's script for the tts_* treatments; null = no prompt. |
campaign.ivr_menu | object | no | The IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu. |
campaign.deferred_bridge | boolean | no | |
campaign.consult_config | object | no | The consult-line config as written ({} = disabled). See CampaignWrite.consult_config. |
campaign.carrier | string | null | no | |
campaign.caller_ids | array of string | no | Prior round-robin pool of E.164 strings (used when caller_id_pool is empty). |
campaign.caller_id_pool | array of string (uuid) | no | DID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed). |
campaign.created_at | string (date-time) | no | |
campaign.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Update campaign config (never state)
PATCH /v1/campaigns/{id}Accepts the same fields as create, and ONLY those: any other key — state included — is refused with 400 bad_request naming it, so a typo can never come back 200 with the field ignored. Lifecycle is explicit via the start/pause/stop/archive endpoints.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | |
campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
sms_body | string | no | REQUIRED for sms_blast; must contain the STOP opt-out notice; supports merge vars. |
abandonment_threshold | number | no | |
min_dial_ratio | number | no | |
max_dial_ratio | number | no | |
calling_window_start | string | no | HH:MM:SS. |
calling_window_end | string | no | |
default_timezone | string | no | |
calling_window_destination_local | boolean | no | |
record_calls | boolean | no | |
ai_voice | boolean | no | |
amd_enabled | boolean | null | no | Per-campaign Answering Machine Detection toggle; null (the default) inherits the type profile. Whether a detected machine gets a voicemail drop or a hang-up is still the compliance gates' decision, not this flag's. |
optout_digit | string | no | Exactly ONE DTMF key: pressing it adds the number to the tenant DNC and ends the call. Anything else is 400 invalid naming the field. "" clears it. |
tts_prompt | string | no | Script rendered to audio for the tts_blast/tts_ivr treatments (one render per distinct text). Capped at 2000 characters — several minutes of speech — so the renderer is never handed an unbounded document. "" clears it. |
ivr_menu | object | no | Digit → action map dispatched on a keypress, flat ({"1": "transfer_agent", "9": "optout"}) or nested ({"prompt": "...", "entries": {"1": {"action": "hangup"}, "2": {"submenu": {...}}}}). Keys are single DTMF keys; actions are transfer_agent, optout, hangup; depth ≤ 5, ≤ 64 nodes. A menu that does not validate is 400 invalid with the engine's reason (Dialer.IVR.Menu.validate/1) — it is never stored. {} = no menu. |
deferred_bridge | boolean | no | Agent campaigns: park the consumer on answer and bridge to the agent only after a HUMAN AMD verdict. Effective only while AMD is on for the campaign. |
consult_config | object | no | Consult-line IVR config, CLOSED schema: factor (one of account_last4, ssn_last4, dob, pin, zip), optional max_attempts (1..10), optional expose (subset of balance, account_status, minimum_payment, due_date, last4_account), optional factor_opts (length positive integer, dob_format mmdd|mmddyyyy). Any other key is 400 invalid naming it. {} = disabled. |
carrier | string | no | |
caller_ids | array of string | no | |
caller_id_pool | array of string (uuid) | no |
{
"record_calls": false
}Responses
200
Updated campaign.
| Field | Type | Required | Description |
|---|---|---|---|
campaign | object (Campaign) | yes | |
campaign.id | string (uuid) | no | |
campaign.tenant_id | string (uuid) | no | |
campaign.name | string | no | |
campaign.state | draft · running · paused · completed · archived · suspended_balance | no | |
campaign.campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
campaign.channel | voice · sms | no | Derived from the type profile — which runner the lifecycle dispatches to. |
campaign.sms_body | string | null | no | |
campaign.abandonment_threshold | string | null | no | Decimal rendered as a JSON string. |
campaign.min_dial_ratio | string | null | no | |
campaign.max_dial_ratio | string | null | no | |
campaign.calling_window_start | string | null | no | HH:MM:SS. |
campaign.calling_window_end | string | null | no | |
campaign.default_timezone | string | null | no | IANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false. |
campaign.calling_window_destination_local | boolean | no | Read calling_window_start/_end in the DESTINATION's local hour instead of default_timezone. false by default, which is the behaviour every campaign had before this field existed. When true the window must be open in EVERY timezone the destination could be in, and a destination whose zone cannot be resolved does not open it. This is a tenant-defined bound only: it can narrow the LEGAL quiet-hours window, never widen it. |
campaign.record_calls | boolean | no | |
campaign.ai_voice | boolean | no | |
campaign.amd_enabled | boolean | null | no | The STORED per-campaign Answering Machine Detection toggle. null = inherit the type profile's default (on for blaster/TTS/IVR/AI types, off for agent types). |
campaign.optout_digit | string | null | no | The single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key. |
campaign.tts_prompt | string | null | no | The operator's script for the tts_* treatments; null = no prompt. |
campaign.ivr_menu | object | no | The IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu. |
campaign.deferred_bridge | boolean | no | |
campaign.consult_config | object | no | The consult-line config as written ({} = disabled). See CampaignWrite.consult_config. |
campaign.carrier | string | null | no | |
campaign.caller_ids | array of string | no | Prior round-robin pool of E.164 strings (used when caller_id_pool is empty). |
campaign.caller_id_pool | array of string (uuid) | no | DID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed). |
campaign.created_at | string (date-time) | no | |
campaign.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Start a campaign
POST /v1/campaigns/{id}/startPersists running FIRST, then spawns the runner for the campaign's channel (voice → dial loop, sms_blast → SMS send loop; same lifecycle API, distinct registry key spaces). Optional runner opts are validated fail-closed (400 on bad types). Idempotent if already running. Invalid transition → 409.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
tick_ms | integer | no | |
batch_size | integer | no | |
msgs_per_tick | integer | no | SMS campaigns — default 1 (≈1 MPS, the long-code-safe 10DLC throughput). |
gateway | string | no | Sofia profile identifier. |
agent_domain | string | no | |
agent_sip_proxy | string | no | "" opts the campaign out of the deployment-wide proxy. |
call_ttl_ms | integer | no | |
blocked_cooldown_ms | integer | no |
{
"tick_ms": 1000,
"batch_size": 2,
"gateway": "default"
}Responses
200
The campaign, now running.
| Field | Type | Required | Description |
|---|---|---|---|
campaign | object (Campaign) | yes | |
campaign.id | string (uuid) | no | |
campaign.tenant_id | string (uuid) | no | |
campaign.name | string | no | |
campaign.state | draft · running · paused · completed · archived · suspended_balance | no | |
campaign.campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
campaign.channel | voice · sms | no | Derived from the type profile — which runner the lifecycle dispatches to. |
campaign.sms_body | string | null | no | |
campaign.abandonment_threshold | string | null | no | Decimal rendered as a JSON string. |
campaign.min_dial_ratio | string | null | no | |
campaign.max_dial_ratio | string | null | no | |
campaign.calling_window_start | string | null | no | HH:MM:SS. |
campaign.calling_window_end | string | null | no | |
campaign.default_timezone | string | null | no | IANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false. |
campaign.calling_window_destination_local | boolean | no | Read calling_window_start/_end in the DESTINATION's local hour instead of default_timezone. false by default, which is the behaviour every campaign had before this field existed. When true the window must be open in EVERY timezone the destination could be in, and a destination whose zone cannot be resolved does not open it. This is a tenant-defined bound only: it can narrow the LEGAL quiet-hours window, never widen it. |
campaign.record_calls | boolean | no | |
campaign.ai_voice | boolean | no | |
campaign.amd_enabled | boolean | null | no | The STORED per-campaign Answering Machine Detection toggle. null = inherit the type profile's default (on for blaster/TTS/IVR/AI types, off for agent types). |
campaign.optout_digit | string | null | no | The single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key. |
campaign.tts_prompt | string | null | no | The operator's script for the tts_* treatments; null = no prompt. |
campaign.ivr_menu | object | no | The IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu. |
campaign.deferred_bridge | boolean | no | |
campaign.consult_config | object | no | The consult-line config as written ({} = disabled). See CampaignWrite.consult_config. |
campaign.carrier | string | null | no | |
campaign.caller_ids | array of string | no | Prior round-robin pool of E.164 strings (used when caller_id_pool is empty). |
campaign.caller_id_pool | array of string (uuid) | no | DID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed). |
campaign.created_at | string (date-time) | no | |
campaign.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Resume a paused campaign (alias of start)
POST /v1/campaigns/{id}/resumeResume IS start (paused → running) — one semantics, two spellings (docs parity).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
tick_ms | integer | no | |
batch_size | integer | no | |
msgs_per_tick | integer | no | SMS campaigns — default 1 (≈1 MPS, the long-code-safe 10DLC throughput). |
gateway | string | no | Sofia profile identifier. |
agent_domain | string | no | |
agent_sip_proxy | string | no | "" opts the campaign out of the deployment-wide proxy. |
call_ttl_ms | integer | no | |
blocked_cooldown_ms | integer | no |
Responses
200
The campaign, now running.
| Field | Type | Required | Description |
|---|---|---|---|
campaign | object (Campaign) | yes | |
campaign.id | string (uuid) | no | |
campaign.tenant_id | string (uuid) | no | |
campaign.name | string | no | |
campaign.state | draft · running · paused · completed · archived · suspended_balance | no | |
campaign.campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
campaign.channel | voice · sms | no | Derived from the type profile — which runner the lifecycle dispatches to. |
campaign.sms_body | string | null | no | |
campaign.abandonment_threshold | string | null | no | Decimal rendered as a JSON string. |
campaign.min_dial_ratio | string | null | no | |
campaign.max_dial_ratio | string | null | no | |
campaign.calling_window_start | string | null | no | HH:MM:SS. |
campaign.calling_window_end | string | null | no | |
campaign.default_timezone | string | null | no | IANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false. |
campaign.calling_window_destination_local | boolean | no | Read calling_window_start/_end in the DESTINATION's local hour instead of default_timezone. false by default, which is the behaviour every campaign had before this field existed. When true the window must be open in EVERY timezone the destination could be in, and a destination whose zone cannot be resolved does not open it. This is a tenant-defined bound only: it can narrow the LEGAL quiet-hours window, never widen it. |
campaign.record_calls | boolean | no | |
campaign.ai_voice | boolean | no | |
campaign.amd_enabled | boolean | null | no | The STORED per-campaign Answering Machine Detection toggle. null = inherit the type profile's default (on for blaster/TTS/IVR/AI types, off for agent types). |
campaign.optout_digit | string | null | no | The single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key. |
campaign.tts_prompt | string | null | no | The operator's script for the tts_* treatments; null = no prompt. |
campaign.ivr_menu | object | no | The IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu. |
campaign.deferred_bridge | boolean | no | |
campaign.consult_config | object | no | The consult-line config as written ({} = disabled). See CampaignWrite.consult_config. |
campaign.carrier | string | null | no | |
campaign.caller_ids | array of string | no | Prior round-robin pool of E.164 strings (used when caller_id_pool is empty). |
campaign.caller_id_pool | array of string (uuid) | no | DID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed). |
campaign.created_at | string (date-time) | no | |
campaign.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Pause a running campaign
POST /v1/campaigns/{id}/pausePersists paused and stops the runner; in-flight calls finish via the CDR pipeline.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The campaign, now paused.
| Field | Type | Required | Description |
|---|---|---|---|
campaign | object (Campaign) | yes | |
campaign.id | string (uuid) | no | |
campaign.tenant_id | string (uuid) | no | |
campaign.name | string | no | |
campaign.state | draft · running · paused · completed · archived · suspended_balance | no | |
campaign.campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
campaign.channel | voice · sms | no | Derived from the type profile — which runner the lifecycle dispatches to. |
campaign.sms_body | string | null | no | |
campaign.abandonment_threshold | string | null | no | Decimal rendered as a JSON string. |
campaign.min_dial_ratio | string | null | no | |
campaign.max_dial_ratio | string | null | no | |
campaign.calling_window_start | string | null | no | HH:MM:SS. |
campaign.calling_window_end | string | null | no | |
campaign.default_timezone | string | null | no | IANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false. |
campaign.calling_window_destination_local | boolean | no | Read calling_window_start/_end in the DESTINATION's local hour instead of default_timezone. false by default, which is the behaviour every campaign had before this field existed. When true the window must be open in EVERY timezone the destination could be in, and a destination whose zone cannot be resolved does not open it. This is a tenant-defined bound only: it can narrow the LEGAL quiet-hours window, never widen it. |
campaign.record_calls | boolean | no | |
campaign.ai_voice | boolean | no | |
campaign.amd_enabled | boolean | null | no | The STORED per-campaign Answering Machine Detection toggle. null = inherit the type profile's default (on for blaster/TTS/IVR/AI types, off for agent types). |
campaign.optout_digit | string | null | no | The single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key. |
campaign.tts_prompt | string | null | no | The operator's script for the tts_* treatments; null = no prompt. |
campaign.ivr_menu | object | no | The IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu. |
campaign.deferred_bridge | boolean | no | |
campaign.consult_config | object | no | The consult-line config as written ({} = disabled). See CampaignWrite.consult_config. |
campaign.carrier | string | null | no | |
campaign.caller_ids | array of string | no | Prior round-robin pool of E.164 strings (used when caller_id_pool is empty). |
campaign.caller_id_pool | array of string (uuid) | no | DID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed). |
campaign.created_at | string (date-time) | no | |
campaign.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Stop a campaign (terminal completed)
POST /v1/campaigns/{id}/stopParameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The campaign, now completed.
| Field | Type | Required | Description |
|---|---|---|---|
campaign | object (Campaign) | yes | |
campaign.id | string (uuid) | no | |
campaign.tenant_id | string (uuid) | no | |
campaign.name | string | no | |
campaign.state | draft · running · paused · completed · archived · suspended_balance | no | |
campaign.campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
campaign.channel | voice · sms | no | Derived from the type profile — which runner the lifecycle dispatches to. |
campaign.sms_body | string | null | no | |
campaign.abandonment_threshold | string | null | no | Decimal rendered as a JSON string. |
campaign.min_dial_ratio | string | null | no | |
campaign.max_dial_ratio | string | null | no | |
campaign.calling_window_start | string | null | no | HH:MM:SS. |
campaign.calling_window_end | string | null | no | |
campaign.default_timezone | string | null | no | IANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false. |
campaign.calling_window_destination_local | boolean | no | Read calling_window_start/_end in the DESTINATION's local hour instead of default_timezone. false by default, which is the behaviour every campaign had before this field existed. When true the window must be open in EVERY timezone the destination could be in, and a destination whose zone cannot be resolved does not open it. This is a tenant-defined bound only: it can narrow the LEGAL quiet-hours window, never widen it. |
campaign.record_calls | boolean | no | |
campaign.ai_voice | boolean | no | |
campaign.amd_enabled | boolean | null | no | The STORED per-campaign Answering Machine Detection toggle. null = inherit the type profile's default (on for blaster/TTS/IVR/AI types, off for agent types). |
campaign.optout_digit | string | null | no | The single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key. |
campaign.tts_prompt | string | null | no | The operator's script for the tts_* treatments; null = no prompt. |
campaign.ivr_menu | object | no | The IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu. |
campaign.deferred_bridge | boolean | no | |
campaign.consult_config | object | no | The consult-line config as written ({} = disabled). See CampaignWrite.consult_config. |
campaign.carrier | string | null | no | |
campaign.caller_ids | array of string | no | Prior round-robin pool of E.164 strings (used when caller_id_pool is empty). |
campaign.caller_id_pool | array of string (uuid) | no | DID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed). |
campaign.created_at | string (date-time) | no | |
campaign.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Archive a campaign
POST /v1/campaigns/{id}/archivedraft|completed → archived (terminal). Anything else → 409.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The campaign, now archived.
| Field | Type | Required | Description |
|---|---|---|---|
campaign | object (Campaign) | yes | |
campaign.id | string (uuid) | no | |
campaign.tenant_id | string (uuid) | no | |
campaign.name | string | no | |
campaign.state | draft · running · paused · completed · archived · suspended_balance | no | |
campaign.campaign_type | predictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blast | no | |
campaign.channel | voice · sms | no | Derived from the type profile — which runner the lifecycle dispatches to. |
campaign.sms_body | string | null | no | |
campaign.abandonment_threshold | string | null | no | Decimal rendered as a JSON string. |
campaign.min_dial_ratio | string | null | no | |
campaign.max_dial_ratio | string | null | no | |
campaign.calling_window_start | string | null | no | HH:MM:SS. |
campaign.calling_window_end | string | null | no | |
campaign.default_timezone | string | null | no | IANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false. |
campaign.calling_window_destination_local | boolean | no | Read calling_window_start/_end in the DESTINATION's local hour instead of default_timezone. false by default, which is the behaviour every campaign had before this field existed. When true the window must be open in EVERY timezone the destination could be in, and a destination whose zone cannot be resolved does not open it. This is a tenant-defined bound only: it can narrow the LEGAL quiet-hours window, never widen it. |
campaign.record_calls | boolean | no | |
campaign.ai_voice | boolean | no | |
campaign.amd_enabled | boolean | null | no | The STORED per-campaign Answering Machine Detection toggle. null = inherit the type profile's default (on for blaster/TTS/IVR/AI types, off for agent types). |
campaign.optout_digit | string | null | no | The single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key. |
campaign.tts_prompt | string | null | no | The operator's script for the tts_* treatments; null = no prompt. |
campaign.ivr_menu | object | no | The IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu. |
campaign.deferred_bridge | boolean | no | |
campaign.consult_config | object | no | The consult-line config as written ({} = disabled). See CampaignWrite.consult_config. |
campaign.carrier | string | null | no | |
campaign.caller_ids | array of string | no | Prior round-robin pool of E.164 strings (used when caller_id_pool is empty). |
campaign.caller_id_pool | array of string (uuid) | no | DID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed). |
campaign.created_at | string (date-time) | no | |
campaign.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Human-initiated dial (preview/manual campaigns)
POST /v1/campaigns/{id}/dialAn agent places ONE call (no auto-pacing) to a debt's primary contact, through the SAME gated Originator as campaigns — all compliance gates and the spend cap apply. A compliance block is a 200 with status: "blocked" (a valid outcome the agent must see, not a server error). Only preview/manual campaign types accept it (422 otherwise).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
debt_id | string (uuid) | yes | |
agent_id | string (uuid) | yes |
{
"debt_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"agent_id": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e"
}Responses
200
Dial outcome — dialing, or blocked by a compliance gate.
| Field | Type | Required | Description |
|---|---|---|---|
status | dialing · blocked | yes | |
call_id | string (uuid) | no | Present when status is dialing. |
reason | tcpa_no_consent · quiet_hours · regf_7in7 · regf_post_contact · state_limit · dnc_listed · consent_revoked · cease_and_desist · number_reassigned · tenant_policy | no | The compliance engine's reason-code vocabulary. |
dialing
{
"status": "dialing",
"call_id": "6f0a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c"
}blocked
{
"status": "blocked",
"reason": "quiet_hours"
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
{
"error": {
"code": "unprocessable",
"message": "scheduled_at must be at least 10 minutes out"
}
}429 — Throttled
Dial rate brake (retryable later with backoff). No Retry-After header is set.
{
"error": {
"code": "throttled",
"message": "dial throttled (rate)"
}
}502 — DialError
The dialer could not place the call (switch rejected the originate).
{
"error": {
"code": "dial_error",
"message": "the dialer could not place the call"
}
}Preview the next dialable lead (without dialing)
GET /v1/campaigns/{id}/next-leadParameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The next dialable lead, or null when the feed is empty.
| Field | Type | Required | Description |
|---|---|---|---|
lead | PreviewLead | null | yes |
{
"lead": {
"debt_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"debt_ref": "cms-debt-0001",
"account_number": "ACC-1001",
"debt_type": "other",
"debt_state": "open",
"contact_phone": "+13125551001",
"line_type": "mobile",
"consumer_state": "IL",
"consumer_timezone": "America/Chicago",
"last_attempt_at": null
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Compliance preflight (dry-run) of a campaign's dialable leads
GET /v1/campaigns/{id}/compliance-preflightRuns the compliance engine in DRY-RUN over the leads the auto-pacer could offer for this campaign (one ranked contact per debt, dialable state, retry watermark elapsed, quarantine applied, agentless deliveries excluded) and returns ONE aggregate keyed by consumer state: which state rule each lead falls under, how many the gates would allow and how many they would block, by blocking rule and reason. Marks nothing and returns no individual lead. Same engine, same Request and same durable stores as dialing, so the counters are what launching the campaign NOW would produce.
Honest about the configurable state gate: with state_matrix_unknown_state_fails_strict OFF for the tenant (the catalog default) a lead with no usable us_state keeps only the federal 7-in-7 — unknown_state_policy and tenant_gates.state_matrix say federal_fallback, never "protected". ON, they say strict / active and the bucket carries the gate's own rule id.
Cost is bounded: at most 10000 leads are evaluated per call; totals.leads is the exact population count and truncated: true says the evaluation covered only the first totals.evaluated of it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
Preflight aggregate
| Field | Type | Required | Description |
|---|---|---|---|
campaign_id | string (uuid) | yes | |
unknown_state_policy | strict · federal_fallback | yes | Posture of the state matrix for a consumer_state outside its canonical shape, per the tenant flag. |
tenant_gates | object | yes | Posture of each gate for this tenant. Only state_matrix has an off switch today. |
tenant_gates.dnc | active | no | |
tenant_gates.consent | active | no | |
tenant_gates.quiet_hours | active | no | |
tenant_gates.reg_f | active | no | |
tenant_gates.state_matrix | active · federal_fallback | no | |
totals | object | yes | |
totals.leads | integer | no | Exact size of the dialable population. |
totals.evaluated | integer | no | Leads the engine was run over (≤ leads). |
totals.eligible | integer | no | |
totals.suppressed | integer | no | |
totals.unknown_state | integer | no | Evaluated leads with an empty us_state. |
truncated | boolean | yes | true when evaluated < leads (evaluation bound hit). |
states | object | yes | Keyed by the RAW us_state the engine sees ("unknown" for the empty string). |
{
"campaign_id": "0190b2a4-7c3e-7f21-9d2a-3f1c2b9e8a10",
"unknown_state_policy": "federal_fallback",
"tenant_gates": {
"dnc": "active",
"consent": "active",
"quiet_hours": "active",
"reg_f": "active",
"state_matrix": "federal_fallback"
},
"totals": {
"leads": 4,
"evaluated": 4,
"eligible": 3,
"suppressed": 1,
"unknown_state": 1
},
"truncated": false,
"states": {
"MA": {
"leads": 1,
"eligible": 0,
"suppressed": 1,
"rules": {
"state.ma.2in7.v1": 1
},
"blocks": {
"state.ma.2in7.v1": {
"state_limit": 1
}
}
},
"WA": {
"leads": 1,
"eligible": 1,
"suppressed": 0,
"rules": {
"state.wa.3in7.v1": 1
},
"blocks": {}
},
"NY": {
"leads": 1,
"eligible": 1,
"suppressed": 0,
"rules": {
"city.nyc.2in7.v1": 1
},
"blocks": {}
},
"unknown": {
"leads": 1,
"eligible": 1,
"suppressed": 0,
"rules": {
"federal_fallback": 1
},
"blocks": {}
}
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Live runner + pacing counters
GET /v1/campaigns/{id}/statsrunning: false, stats: null when no runner is up. Voice runners report {dialed, blocked, finished, reaped, throttled, in_flight, pacing}; SMS runners report {sent, duplicate, blocked, failed, pending_cooldown}.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
Campaign state + live stats.
| Field | Type | Required | Description |
|---|---|---|---|
campaign_id | string (uuid) | yes | |
state | draft · running · paused · completed · archived · suspended_balance | yes | |
running | boolean | yes | |
stats | VoiceRunnerStats | SmsRunnerStats | null | yes |
{
"campaign_id": "3f2c1b0a-9d8e-4c7b-a6f5-4e3d2c1b0a99",
"state": "running",
"running": true,
"stats": {
"dialed": 42,
"blocked": 5,
"finished": 30,
"reaped": 0,
"in_flight": 7,
"pacing": {
"ratio": 1.6,
"abandoned": 1,
"connected": 22,
"abandonment_rate": 0.0435,
"tokens": 3
}
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Survey / IVR keypress distribution
GET /v1/campaigns/{id}/survey-resultsThe read side of the agentless survey responses: how many consumers pressed each DTMF key on this campaign over [from, to] (both inclusive; default trailing 30 UTC days), optionally narrowed to one question_id (q1 is the default question the call FSM records). distribution is sorted by digit; total is the sum. Read-only and tenant-scoped: a campaign of another tenant is 404, never an empty 200. ?format=csv (or Accept: text/csv) returns the same numbers as a CSV artifact with its own X-Artifact-SHA256 — same path as /v1/stats/ai-usage and /v1/reports/violations-prevented.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
from | query | string (date-time) | no | RFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted). |
to | query | string (date-time) | no | RFC 3339 upper bound. |
question_id | query | string | no | Narrow to one question (non-empty string). Omitted = every question. |
format | query | csv | no | csv for the downloadable artifact (equivalent: Accept: text/csv). |
Responses
200
Digit distribution (JSON) or the CSV artifact.
| Field | Type | Required | Description |
|---|---|---|---|
campaign_id | string (uuid) | yes | |
from | string (date-time) | yes | |
to | string (date-time) | yes | |
question_id | string | null | yes | The filter that was applied; null = every question. |
total | integer | yes | |
distribution | array of object | yes | One row per digit pressed at least once, sorted by digit. |
distribution[].digit | string | yes | |
distribution[].count | integer | yes |
{
"campaign_id": "3f2c1b0a-9d8e-4c7b-a6f5-4e3d2c1b0a99",
"from": "2026-06-01T00:00:00Z",
"to": "2026-06-30T23:59:59Z",
"question_id": null,
"total": 4,
"distribution": [
{
"digit": "1",
"count": 2
},
{
"digit": "2",
"count": 1
},
{
"digit": "9",
"count": 1
}
]
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Rewind dry-run (filters in query)
GET /v1/campaigns/{id}/rewind/previewDashboard-friendly GET twin of the POST preview. excluded_by_compliance counts matching debts the engine would block RIGHT NOW (informational; the gates re-enforce at every originate anyway).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
dispositions | query | string | no | Comma-separated list matched against each debt's LATEST disposition. |
max_attempts | query | integer | no | |
last_attempt_older_than_days | query | integer | no |
Responses
200
Dry-run result.
| Field | Type | Required | Description |
|---|---|---|---|
matching | integer | yes | |
excluded_by_compliance | integer | yes | Matching debts the engine would block RIGHT NOW (informational). |
debt_ids | array of string (uuid) | yes |
{
"matching": 12,
"excluded_by_compliance": 3,
"debt_ids": [
"5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d"
]
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
{
"error": {
"code": "unprocessable",
"message": "scheduled_at must be at least 10 minutes out"
}
}Rewind dry-run (filters in body)
POST /v1/campaigns/{id}/rewind/previewSame dry-run as the GET, filters in the JSON body (B1 handoff shape).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
dispositions | array of string | no | Matched against each debt's LATEST disposition. |
max_attempts | integer | no | Count of original attempts. |
last_attempt_older_than_days | integer | no | |
keep_scheduled_callbacks | boolean | no | false cancels the campaign's pending callbacks for the rewound debts (rewind only). |
amd_verdicts | array of string | no | RESERVED — always answers 422 amd_filter_not_supported. |
{
"max_attempts": 1
}Responses
200
Dry-run result.
| Field | Type | Required | Description |
|---|---|---|---|
matching | integer | yes | |
excluded_by_compliance | integer | yes | Matching debts the engine would block RIGHT NOW (informational). |
debt_ids | array of string (uuid) | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
{
"error": {
"code": "unprocessable",
"message": "scheduled_at must be at least 10 minutes out"
}
}Re-queue matching exhausted debts
POST /v1/campaigns/{id}/rewindSets the per-debt requeued_at watermark (the lead feed treats them as never-attempted) and resurrects closed → open; paid/settled/disputed are NEVER rewound and the CDR is untouched (append-only; Reg F counters keep counting). keep_scheduled_callbacks: false cancels the campaign's pending callbacks for the rewound debts. amd_verdicts is RESERVED → 422.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
dispositions | array of string | no | Matched against each debt's LATEST disposition. |
max_attempts | integer | no | Count of original attempts. |
last_attempt_older_than_days | integer | no | |
keep_scheduled_callbacks | boolean | no | false cancels the campaign's pending callbacks for the rewound debts (rewind only). |
amd_verdicts | array of string | no | RESERVED — always answers 422 amd_filter_not_supported. |
{
"max_attempts": 1,
"keep_scheduled_callbacks": true
}Responses
200
Rewind applied.
| Field | Type | Required | Description |
|---|---|---|---|
requeued | integer | yes | |
callbacks_cancelled | integer | yes |
{
"requeued": 9,
"callbacks_cancelled": 0
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
{
"error": {
"code": "unprocessable",
"message": "scheduled_at must be at least 10 minutes out"
}
}