Skip to content

Campaigns

Generated from DialerDigital/core commit 6e9f026f0a72 — spec openapi/dialer-v1.yaml v0.36.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.

Campaign CRUD, lifecycle, manual dial, rewind.

Endpoints

MethodPathSummary
GET/v1/campaignsList campaigns
POST/v1/campaignsCreate a campaign (state draft)
GET/v1/campaigns/{id}Fetch a campaign
PATCH/v1/campaigns/{id}Update campaign config (never state)
POST/v1/campaigns/{id}/startStart a campaign
POST/v1/campaigns/{id}/resumeResume a paused campaign (alias of start)
POST/v1/campaigns/{id}/pausePause a running campaign
POST/v1/campaigns/{id}/stopStop a campaign (terminal completed)
POST/v1/campaigns/{id}/archiveArchive a campaign
POST/v1/campaigns/{id}/dialHuman-initiated dial (preview/manual campaigns)
GET/v1/campaigns/{id}/next-leadPreview the next dialable lead (without dialing)
GET/v1/campaigns/{id}/compliance-preflightCompliance preflight (dry-run) of a campaign's dialable leads
GET/v1/campaigns/{id}/statsLive runner + pacing counters
GET/v1/campaigns/{id}/survey-resultsSurvey / IVR keypress distribution
GET/v1/campaigns/{id}/rewind/previewRewind dry-run (filters in query)
POST/v1/campaigns/{id}/rewind/previewRewind dry-run (filters in body)
POST/v1/campaigns/{id}/rewindRe-queue matching exhausted debts

List campaigns

http
GET /v1/campaigns

Responses

200

All campaigns of the tenant.

FieldTypeRequiredDescription
campaignsarray of Campaignyes
campaigns[].idstring (uuid)no
campaigns[].tenant_idstring (uuid)no
campaigns[].namestringno
campaigns[].statedraft · running · paused · completed · archived · suspended_balanceno
campaigns[].campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
campaigns[].channelvoice · smsnoDerived from the type profile — which runner the lifecycle dispatches to.
campaigns[].sms_bodystring | nullno
campaigns[].abandonment_thresholdstring | nullnoDecimal rendered as a JSON string.
campaigns[].min_dial_ratiostring | nullno
campaigns[].max_dial_ratiostring | nullno
campaigns[].calling_window_startstring | nullnoHH:MM:SS.
campaigns[].calling_window_endstring | nullno
campaigns[].default_timezonestring | nullnoIANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false.
campaigns[].calling_window_destination_localbooleannoRead 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_callsbooleanno
campaigns[].ai_voicebooleanno
campaigns[].amd_enabledboolean | nullnoThe 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_digitstring | nullnoThe single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key.
campaigns[].tts_promptstring | nullnoThe operator's script for the tts_* treatments; null = no prompt.
campaigns[].ivr_menuobjectnoThe IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu.
campaigns[].deferred_bridgebooleanno
campaigns[].consult_configobjectnoThe consult-line config as written ({} = disabled). See CampaignWrite.consult_config.
campaigns[].carrierstring | nullno
campaigns[].caller_idsarray of stringnoPrior round-robin pool of E.164 strings (used when caller_id_pool is empty).
campaigns[].caller_id_poolarray of string (uuid)noDID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed).
campaigns[].created_atstring (date-time)no
campaigns[].updated_atstring (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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

Create a campaign (state draft)

http
POST /v1/campaigns

campaign_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 name409 conflict.

Request body (JSON, required)

FieldTypeRequiredDescription
namestringno
campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
sms_bodystringnoREQUIRED for sms_blast; must contain the STOP opt-out notice; supports merge vars.
abandonment_thresholdnumberno
min_dial_rationumberno
max_dial_rationumberno
calling_window_startstringnoHH:MM:SS.
calling_window_endstringno
default_timezonestringno
calling_window_destination_localbooleanno
record_callsbooleanno
ai_voicebooleanno
amd_enabledboolean | nullnoPer-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_digitstringnoExactly 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_promptstringnoScript 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_menuobjectnoDigit → 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_bridgebooleannoAgent 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_configobjectnoConsult-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.
carrierstringno
caller_idsarray of stringno
caller_id_poolarray of string (uuid)no
json
{
  "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).

FieldTypeRequiredDescription
campaignobject (Campaign)yes
campaign.idstring (uuid)no
campaign.tenant_idstring (uuid)no
campaign.namestringno
campaign.statedraft · running · paused · completed · archived · suspended_balanceno
campaign.campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
campaign.channelvoice · smsnoDerived from the type profile — which runner the lifecycle dispatches to.
campaign.sms_bodystring | nullno
campaign.abandonment_thresholdstring | nullnoDecimal rendered as a JSON string.
campaign.min_dial_ratiostring | nullno
campaign.max_dial_ratiostring | nullno
campaign.calling_window_startstring | nullnoHH:MM:SS.
campaign.calling_window_endstring | nullno
campaign.default_timezonestring | nullnoIANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false.
campaign.calling_window_destination_localbooleannoRead 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_callsbooleanno
campaign.ai_voicebooleanno
campaign.amd_enabledboolean | nullnoThe 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_digitstring | nullnoThe single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key.
campaign.tts_promptstring | nullnoThe operator's script for the tts_* treatments; null = no prompt.
campaign.ivr_menuobjectnoThe IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu.
campaign.deferred_bridgebooleanno
campaign.consult_configobjectnoThe consult-line config as written ({} = disabled). See CampaignWrite.consult_config.
campaign.carrierstring | nullno
campaign.caller_idsarray of stringnoPrior round-robin pool of E.164 strings (used when caller_id_pool is empty).
campaign.caller_id_poolarray of string (uuid)noDID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed).
campaign.created_atstring (date-time)no
campaign.updated_atstring (date-time)no
json
{
  "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).

json
{
  "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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

409 — Conflict

Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

Fetch a campaign

http
GET /v1/campaigns/{id}

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The campaign.

FieldTypeRequiredDescription
campaignobject (Campaign)yes
campaign.idstring (uuid)no
campaign.tenant_idstring (uuid)no
campaign.namestringno
campaign.statedraft · running · paused · completed · archived · suspended_balanceno
campaign.campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
campaign.channelvoice · smsnoDerived from the type profile — which runner the lifecycle dispatches to.
campaign.sms_bodystring | nullno
campaign.abandonment_thresholdstring | nullnoDecimal rendered as a JSON string.
campaign.min_dial_ratiostring | nullno
campaign.max_dial_ratiostring | nullno
campaign.calling_window_startstring | nullnoHH:MM:SS.
campaign.calling_window_endstring | nullno
campaign.default_timezonestring | nullnoIANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false.
campaign.calling_window_destination_localbooleannoRead 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_callsbooleanno
campaign.ai_voicebooleanno
campaign.amd_enabledboolean | nullnoThe 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_digitstring | nullnoThe single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key.
campaign.tts_promptstring | nullnoThe operator's script for the tts_* treatments; null = no prompt.
campaign.ivr_menuobjectnoThe IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu.
campaign.deferred_bridgebooleanno
campaign.consult_configobjectnoThe consult-line config as written ({} = disabled). See CampaignWrite.consult_config.
campaign.carrierstring | nullno
campaign.caller_idsarray of stringnoPrior round-robin pool of E.164 strings (used when caller_id_pool is empty).
campaign.caller_id_poolarray of string (uuid)noDID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed).
campaign.created_atstring (date-time)no
campaign.updated_atstring (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).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Update campaign config (never state)

http
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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
namestringno
campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
sms_bodystringnoREQUIRED for sms_blast; must contain the STOP opt-out notice; supports merge vars.
abandonment_thresholdnumberno
min_dial_rationumberno
max_dial_rationumberno
calling_window_startstringnoHH:MM:SS.
calling_window_endstringno
default_timezonestringno
calling_window_destination_localbooleanno
record_callsbooleanno
ai_voicebooleanno
amd_enabledboolean | nullnoPer-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_digitstringnoExactly 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_promptstringnoScript 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_menuobjectnoDigit → 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_bridgebooleannoAgent 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_configobjectnoConsult-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.
carrierstringno
caller_idsarray of stringno
caller_id_poolarray of string (uuid)no
json
{
  "record_calls": false
}

Responses

200

Updated campaign.

FieldTypeRequiredDescription
campaignobject (Campaign)yes
campaign.idstring (uuid)no
campaign.tenant_idstring (uuid)no
campaign.namestringno
campaign.statedraft · running · paused · completed · archived · suspended_balanceno
campaign.campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
campaign.channelvoice · smsnoDerived from the type profile — which runner the lifecycle dispatches to.
campaign.sms_bodystring | nullno
campaign.abandonment_thresholdstring | nullnoDecimal rendered as a JSON string.
campaign.min_dial_ratiostring | nullno
campaign.max_dial_ratiostring | nullno
campaign.calling_window_startstring | nullnoHH:MM:SS.
campaign.calling_window_endstring | nullno
campaign.default_timezonestring | nullnoIANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false.
campaign.calling_window_destination_localbooleannoRead 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_callsbooleanno
campaign.ai_voicebooleanno
campaign.amd_enabledboolean | nullnoThe 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_digitstring | nullnoThe single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key.
campaign.tts_promptstring | nullnoThe operator's script for the tts_* treatments; null = no prompt.
campaign.ivr_menuobjectnoThe IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu.
campaign.deferred_bridgebooleanno
campaign.consult_configobjectnoThe consult-line config as written ({} = disabled). See CampaignWrite.consult_config.
campaign.carrierstring | nullno
campaign.caller_idsarray of stringnoPrior round-robin pool of E.164 strings (used when caller_id_pool is empty).
campaign.caller_id_poolarray of string (uuid)noDID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed).
campaign.created_atstring (date-time)no
campaign.updated_atstring (date-time)no

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "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).

json
{
  "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.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

Start a campaign

http
POST /v1/campaigns/{id}/start

Persists 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON)

FieldTypeRequiredDescription
tick_msintegerno
batch_sizeintegerno
msgs_per_tickintegernoSMS campaigns — default 1 (≈1 MPS, the long-code-safe 10DLC throughput).
gatewaystringnoSofia profile identifier.
agent_domainstringno
agent_sip_proxystringno"" opts the campaign out of the deployment-wide proxy.
call_ttl_msintegerno
blocked_cooldown_msintegerno
json
{
  "tick_ms": 1000,
  "batch_size": 2,
  "gateway": "default"
}

Responses

200

The campaign, now running.

FieldTypeRequiredDescription
campaignobject (Campaign)yes
campaign.idstring (uuid)no
campaign.tenant_idstring (uuid)no
campaign.namestringno
campaign.statedraft · running · paused · completed · archived · suspended_balanceno
campaign.campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
campaign.channelvoice · smsnoDerived from the type profile — which runner the lifecycle dispatches to.
campaign.sms_bodystring | nullno
campaign.abandonment_thresholdstring | nullnoDecimal rendered as a JSON string.
campaign.min_dial_ratiostring | nullno
campaign.max_dial_ratiostring | nullno
campaign.calling_window_startstring | nullnoHH:MM:SS.
campaign.calling_window_endstring | nullno
campaign.default_timezonestring | nullnoIANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false.
campaign.calling_window_destination_localbooleannoRead 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_callsbooleanno
campaign.ai_voicebooleanno
campaign.amd_enabledboolean | nullnoThe 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_digitstring | nullnoThe single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key.
campaign.tts_promptstring | nullnoThe operator's script for the tts_* treatments; null = no prompt.
campaign.ivr_menuobjectnoThe IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu.
campaign.deferred_bridgebooleanno
campaign.consult_configobjectnoThe consult-line config as written ({} = disabled). See CampaignWrite.consult_config.
campaign.carrierstring | nullno
campaign.caller_idsarray of stringnoPrior round-robin pool of E.164 strings (used when caller_id_pool is empty).
campaign.caller_id_poolarray of string (uuid)noDID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed).
campaign.created_atstring (date-time)no
campaign.updated_atstring (date-time)no

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "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).

json
{
  "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.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

Resume a paused campaign (alias of start)

http
POST /v1/campaigns/{id}/resume

Resume IS start (pausedrunning) — one semantics, two spellings (docs parity).

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON)

FieldTypeRequiredDescription
tick_msintegerno
batch_sizeintegerno
msgs_per_tickintegernoSMS campaigns — default 1 (≈1 MPS, the long-code-safe 10DLC throughput).
gatewaystringnoSofia profile identifier.
agent_domainstringno
agent_sip_proxystringno"" opts the campaign out of the deployment-wide proxy.
call_ttl_msintegerno
blocked_cooldown_msintegerno

Responses

200

The campaign, now running.

FieldTypeRequiredDescription
campaignobject (Campaign)yes
campaign.idstring (uuid)no
campaign.tenant_idstring (uuid)no
campaign.namestringno
campaign.statedraft · running · paused · completed · archived · suspended_balanceno
campaign.campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
campaign.channelvoice · smsnoDerived from the type profile — which runner the lifecycle dispatches to.
campaign.sms_bodystring | nullno
campaign.abandonment_thresholdstring | nullnoDecimal rendered as a JSON string.
campaign.min_dial_ratiostring | nullno
campaign.max_dial_ratiostring | nullno
campaign.calling_window_startstring | nullnoHH:MM:SS.
campaign.calling_window_endstring | nullno
campaign.default_timezonestring | nullnoIANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false.
campaign.calling_window_destination_localbooleannoRead 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_callsbooleanno
campaign.ai_voicebooleanno
campaign.amd_enabledboolean | nullnoThe 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_digitstring | nullnoThe single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key.
campaign.tts_promptstring | nullnoThe operator's script for the tts_* treatments; null = no prompt.
campaign.ivr_menuobjectnoThe IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu.
campaign.deferred_bridgebooleanno
campaign.consult_configobjectnoThe consult-line config as written ({} = disabled). See CampaignWrite.consult_config.
campaign.carrierstring | nullno
campaign.caller_idsarray of stringnoPrior round-robin pool of E.164 strings (used when caller_id_pool is empty).
campaign.caller_id_poolarray of string (uuid)noDID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed).
campaign.created_atstring (date-time)no
campaign.updated_atstring (date-time)no

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "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).

json
{
  "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.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

Pause a running campaign

http
POST /v1/campaigns/{id}/pause

Persists paused and stops the runner; in-flight calls finish via the CDR pipeline.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The campaign, now paused.

FieldTypeRequiredDescription
campaignobject (Campaign)yes
campaign.idstring (uuid)no
campaign.tenant_idstring (uuid)no
campaign.namestringno
campaign.statedraft · running · paused · completed · archived · suspended_balanceno
campaign.campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
campaign.channelvoice · smsnoDerived from the type profile — which runner the lifecycle dispatches to.
campaign.sms_bodystring | nullno
campaign.abandonment_thresholdstring | nullnoDecimal rendered as a JSON string.
campaign.min_dial_ratiostring | nullno
campaign.max_dial_ratiostring | nullno
campaign.calling_window_startstring | nullnoHH:MM:SS.
campaign.calling_window_endstring | nullno
campaign.default_timezonestring | nullnoIANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false.
campaign.calling_window_destination_localbooleannoRead 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_callsbooleanno
campaign.ai_voicebooleanno
campaign.amd_enabledboolean | nullnoThe 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_digitstring | nullnoThe single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key.
campaign.tts_promptstring | nullnoThe operator's script for the tts_* treatments; null = no prompt.
campaign.ivr_menuobjectnoThe IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu.
campaign.deferred_bridgebooleanno
campaign.consult_configobjectnoThe consult-line config as written ({} = disabled). See CampaignWrite.consult_config.
campaign.carrierstring | nullno
campaign.caller_idsarray of stringnoPrior round-robin pool of E.164 strings (used when caller_id_pool is empty).
campaign.caller_id_poolarray of string (uuid)noDID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed).
campaign.created_atstring (date-time)no
campaign.updated_atstring (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).

json
{
  "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).

json
{
  "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.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

Stop a campaign (terminal completed)

http
POST /v1/campaigns/{id}/stop

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The campaign, now completed.

FieldTypeRequiredDescription
campaignobject (Campaign)yes
campaign.idstring (uuid)no
campaign.tenant_idstring (uuid)no
campaign.namestringno
campaign.statedraft · running · paused · completed · archived · suspended_balanceno
campaign.campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
campaign.channelvoice · smsnoDerived from the type profile — which runner the lifecycle dispatches to.
campaign.sms_bodystring | nullno
campaign.abandonment_thresholdstring | nullnoDecimal rendered as a JSON string.
campaign.min_dial_ratiostring | nullno
campaign.max_dial_ratiostring | nullno
campaign.calling_window_startstring | nullnoHH:MM:SS.
campaign.calling_window_endstring | nullno
campaign.default_timezonestring | nullnoIANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false.
campaign.calling_window_destination_localbooleannoRead 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_callsbooleanno
campaign.ai_voicebooleanno
campaign.amd_enabledboolean | nullnoThe 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_digitstring | nullnoThe single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key.
campaign.tts_promptstring | nullnoThe operator's script for the tts_* treatments; null = no prompt.
campaign.ivr_menuobjectnoThe IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu.
campaign.deferred_bridgebooleanno
campaign.consult_configobjectnoThe consult-line config as written ({} = disabled). See CampaignWrite.consult_config.
campaign.carrierstring | nullno
campaign.caller_idsarray of stringnoPrior round-robin pool of E.164 strings (used when caller_id_pool is empty).
campaign.caller_id_poolarray of string (uuid)noDID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed).
campaign.created_atstring (date-time)no
campaign.updated_atstring (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).

json
{
  "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).

json
{
  "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.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

Archive a campaign

http
POST /v1/campaigns/{id}/archive

draft|completedarchived (terminal). Anything else → 409.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The campaign, now archived.

FieldTypeRequiredDescription
campaignobject (Campaign)yes
campaign.idstring (uuid)no
campaign.tenant_idstring (uuid)no
campaign.namestringno
campaign.statedraft · running · paused · completed · archived · suspended_balanceno
campaign.campaign_typepredictive · progressive · preview · manual · tts_blast · tts_ivr · ivr_cascade · survey · ai_voice · sms_blastno
campaign.channelvoice · smsnoDerived from the type profile — which runner the lifecycle dispatches to.
campaign.sms_bodystring | nullno
campaign.abandonment_thresholdstring | nullnoDecimal rendered as a JSON string.
campaign.min_dial_ratiostring | nullno
campaign.max_dial_ratiostring | nullno
campaign.calling_window_startstring | nullnoHH:MM:SS.
campaign.calling_window_endstring | nullno
campaign.default_timezonestring | nullnoIANA timezone. The zone the OPERATIONAL calling window is read in while calling_window_destination_local is false.
campaign.calling_window_destination_localbooleannoRead 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_callsbooleanno
campaign.ai_voicebooleanno
campaign.amd_enabledboolean | nullnoThe 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_digitstring | nullnoThe single DTMF key that opts the consumer out (0-9, *, #); null = no opt-out key.
campaign.tts_promptstring | nullnoThe operator's script for the tts_* treatments; null = no prompt.
campaign.ivr_menuobjectnoThe IVR menu as written ({} = no menu). See CampaignWrite.ivr_menu.
campaign.deferred_bridgebooleanno
campaign.consult_configobjectnoThe consult-line config as written ({} = disabled). See CampaignWrite.consult_config.
campaign.carrierstring | nullno
campaign.caller_idsarray of stringnoPrior round-robin pool of E.164 strings (used when caller_id_pool is empty).
campaign.caller_id_poolarray of string (uuid)noDID-registry ids; re-resolved every tick to the ACTIVE DIDs — an all-parked pool dials NOTHING (fail-closed).
campaign.created_atstring (date-time)no
campaign.updated_atstring (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).

json
{
  "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).

json
{
  "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.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

Human-initiated dial (preview/manual campaigns)

http
POST /v1/campaigns/{id}/dial

An 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
debt_idstring (uuid)yes
agent_idstring (uuid)yes
json
{
  "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.

FieldTypeRequiredDescription
statusdialing · blockedyes
call_idstring (uuid)noPresent when status is dialing.
reasontcpa_no_consent · quiet_hours · regf_7in7 · regf_post_contact · state_limit · dnc_listed · consent_revoked · cease_and_desist · number_reassigned · tenant_policynoThe compliance engine's reason-code vocabulary.

dialing

json
{
  "status": "dialing",
  "call_id": "6f0a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c"
}

blocked

json
{
  "status": "blocked",
  "reason": "quiet_hours"
}

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

403 — Forbidden

The tenant is suspended.

json
{
  "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).

json
{
  "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.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

422 — Unprocessable

Shape is fine, semantics are not (broken domain rule).

json
{
  "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.

json
{
  "error": {
    "code": "throttled",
    "message": "dial throttled (rate)"
  }
}

502 — DialError

The dialer could not place the call (switch rejected the originate).

json
{
  "error": {
    "code": "dial_error",
    "message": "the dialer could not place the call"
  }
}

Preview the next dialable lead (without dialing)

http
GET /v1/campaigns/{id}/next-lead

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The next dialable lead, or null when the feed is empty.

FieldTypeRequiredDescription
leadPreviewLead | nullyes
json
{
  "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).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Compliance preflight (dry-run) of a campaign's dialable leads

http
GET /v1/campaigns/{id}/compliance-preflight

Runs 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

Preflight aggregate

FieldTypeRequiredDescription
campaign_idstring (uuid)yes
unknown_state_policystrict · federal_fallbackyesPosture of the state matrix for a consumer_state outside its canonical shape, per the tenant flag.
tenant_gatesobjectyesPosture of each gate for this tenant. Only state_matrix has an off switch today.
tenant_gates.dncactiveno
tenant_gates.consentactiveno
tenant_gates.quiet_hoursactiveno
tenant_gates.reg_factiveno
tenant_gates.state_matrixactive · federal_fallbackno
totalsobjectyes
totals.leadsintegernoExact size of the dialable population.
totals.evaluatedintegernoLeads the engine was run over (≤ leads).
totals.eligibleintegerno
totals.suppressedintegerno
totals.unknown_stateintegernoEvaluated leads with an empty us_state.
truncatedbooleanyestrue when evaluated < leads (evaluation bound hit).
statesobjectyesKeyed by the RAW us_state the engine sees ("unknown" for the empty string).
json
{
  "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).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Live runner + pacing counters

http
GET /v1/campaigns/{id}/stats

running: 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

Campaign state + live stats.

FieldTypeRequiredDescription
campaign_idstring (uuid)yes
statedraft · running · paused · completed · archived · suspended_balanceyes
runningbooleanyes
statsVoiceRunnerStats | SmsRunnerStats | nullyes
json
{
  "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).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Survey / IVR keypress distribution

http
GET /v1/campaigns/{id}/survey-results

The 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.
fromquerystring (date-time)noRFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted).
toquerystring (date-time)noRFC 3339 upper bound.
question_idquerystringnoNarrow to one question (non-empty string). Omitted = every question.
formatquerycsvnocsv for the downloadable artifact (equivalent: Accept: text/csv).

Responses

200

Digit distribution (JSON) or the CSV artifact.

FieldTypeRequiredDescription
campaign_idstring (uuid)yes
fromstring (date-time)yes
tostring (date-time)yes
question_idstring | nullyesThe filter that was applied; null = every question.
totalintegeryes
distributionarray of objectyesOne row per digit pressed at least once, sorted by digit.
distribution[].digitstringyes
distribution[].countintegeryes
json
{
  "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).

json
{
  "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).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Rewind dry-run (filters in query)

http
GET /v1/campaigns/{id}/rewind/preview

Dashboard-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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.
dispositionsquerystringnoComma-separated list matched against each debt's LATEST disposition.
max_attemptsqueryintegerno
last_attempt_older_than_daysqueryintegerno

Responses

200

Dry-run result.

FieldTypeRequiredDescription
matchingintegeryes
excluded_by_complianceintegeryesMatching debts the engine would block RIGHT NOW (informational).
debt_idsarray of string (uuid)yes
json
{
  "matching": 12,
  "excluded_by_compliance": 3,
  "debt_ids": [
    "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d"
  ]
}

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

422 — Unprocessable

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at must be at least 10 minutes out"
  }
}

Rewind dry-run (filters in body)

http
POST /v1/campaigns/{id}/rewind/preview

Same dry-run as the GET, filters in the JSON body (B1 handoff shape).

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
dispositionsarray of stringnoMatched against each debt's LATEST disposition.
max_attemptsintegernoCount of original attempts.
last_attempt_older_than_daysintegerno
keep_scheduled_callbacksbooleannofalse cancels the campaign's pending callbacks for the rewound debts (rewind only).
amd_verdictsarray of stringnoRESERVED — always answers 422 amd_filter_not_supported.
json
{
  "max_attempts": 1
}

Responses

200

Dry-run result.

FieldTypeRequiredDescription
matchingintegeryes
excluded_by_complianceintegeryesMatching debts the engine would block RIGHT NOW (informational).
debt_idsarray of string (uuid)yes

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

422 — Unprocessable

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at must be at least 10 minutes out"
  }
}

Re-queue matching exhausted debts

http
POST /v1/campaigns/{id}/rewind

Sets the per-debt requeued_at watermark (the lead feed treats them as never-attempted) and resurrects closedopen; 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
dispositionsarray of stringnoMatched against each debt's LATEST disposition.
max_attemptsintegernoCount of original attempts.
last_attempt_older_than_daysintegerno
keep_scheduled_callbacksbooleannofalse cancels the campaign's pending callbacks for the rewound debts (rewind only).
amd_verdictsarray of stringnoRESERVED — always answers 422 amd_filter_not_supported.
json
{
  "max_attempts": 1,
  "keep_scheduled_callbacks": true
}

Responses

200

Rewind applied.

FieldTypeRequiredDescription
requeuedintegeryes
callbacks_cancelledintegeryes
json
{
  "requeued": 9,
  "callbacks_cancelled": 0
}

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

422 — Unprocessable

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at must be at least 10 minutes out"
  }
}

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.