#Outbound Campaigns
Outbound campaign management routes are mounted under /v1/outbound-campaigns.
These routes manage partner-facing campaign records over the existing AiDial outbound runtime tables.
Production API: https://api.aidial.ai
This page omits environment-specific host names, internal topology, and deployment details.
These routes do not expose raw runtime configuration editing, imported contact phone numbers, imported CSV content, ciphertext, provider credentials, API keys, bearer tokens, or transcripts. Campaign detail can include configured CLI disclosure fields such as cli_return_number because those fields are part of the campaign compliance record, not imported contact data.
#Authentication and Scope
All routes require authentication through the shared API auth layer:
X-API-KeyAuthorization: Bearer <jwt>
If both headers are present, the API key is used and the bearer token is ignored. Missing, invalid, revoked, or expired credentials return 401 Unauthorized.
Bearer authentication requires ZITADEL_ISSUER, ZITADEL_AUDIENCE, and ZITADEL_JWKS_URL; these settings are required and have no fallback defaults. Bearer tenant assignment is resolved from portal_users and portal_user_client_assignments.
Both authentication modes resolve to an AuthContext. This router scopes all campaign queries and mutations to the resolved client_id and data_env; it does not accept a client or data-environment override in the request. Outbound campaign records are read and written through the tenant data access layer using the strict (client_id, data_env) route in the tenant routing map. Authentication, idempotency, project metadata, and route lookup use the local API metadata store on the API host.
Write routes require partner_admin. Read routes allow partner_admin, partner_user, aidial_admin, and aidial_operator within the resolved authenticated scope. Out-of-scope campaigns, invalid campaign ids, and role-denied mutations return non-enumerating 404 behavior.
For bearer partner_admin and partner_user principals, every campaign-scoped route additionally requires the campaign's project to be one of the principal's assigned projects; any other campaign returns 404.
#Routes
| Endpoint | Method | Scope | Purpose |
|---|---|---|---|
/v1/outbound-campaigns | GET | read | List campaign summaries with project, status, call type, and search filters. |
/v1/outbound-campaigns | POST | partner_admin | Create a campaign. |
/v1/outbound-campaigns/{campaign_id} | GET | read | Read safe campaign detail. |
/v1/outbound-campaigns/{campaign_id} | PATCH | partner_admin | Update mutable campaign metadata while state allows edits. |
/v1/outbound-campaigns/{campaign_id}/contacts | GET | read | List masked contact status, outcome, retry, consent, and registry summaries. |
/v1/outbound-campaigns/{campaign_id}/contacts/import | POST | partner_admin | Import CSV contacts. Requires Idempotency-Key. |
/v1/outbound-campaigns/{campaign_id}/contacts/import-json | POST | partner_admin | Import a bounded JSON contact batch. Requires Idempotency-Key. |
/v1/outbound-campaigns/{campaign_id}/registry-wash-records | POST | partner_admin | Attach DNCR/registry wash evidence. Requires Idempotency-Key. |
/v1/outbound-campaigns/{campaign_id}/validation | GET | read | Read launch validation state. |
/v1/outbound-campaigns/{campaign_id}/validate | POST | partner_admin | Recompute and audit launch validation. |
/v1/outbound-campaigns/{campaign_id}/run | POST | partner_admin | Launch after validation passes. Requires Idempotency-Key. |
/v1/outbound-campaigns/{campaign_id}/pause | POST | partner_admin | Pause campaign dispatch. Requires Idempotency-Key. |
/v1/outbound-campaigns/{campaign_id}/resume | POST | partner_admin | Resume after validation passes. Requires Idempotency-Key. |
/v1/outbound-campaigns/{campaign_id}/cancel | POST | partner_admin | Cancel without deleting history. Requires Idempotency-Key. |
/v1/outbound-campaigns/{campaign_id}/report | GET | read | Read reporting aggregates and validation state. |
#Query Parameters
GET /v1/outbound-campaigns supports:
| Name | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number, minimum 1. |
page_size | integer | 20 | Page size, minimum 1, maximum 100. |
sort_by | string | updated_at_utc | One of updated_at_utc, created_at_utc, name, or status. |
sort_order | string | desc | asc or desc. |
project_id | string | - | Filters campaigns to one project within the authenticated client scope. |
status | string | - | Filters by campaign status. |
campaign_call_type | string | - | Filters by telemarketing, research, or service. |
search | string | - | Case-insensitive campaign-name search, maximum 120 characters. |
GET /v1/outbound-campaigns/{campaign_id}/contacts supports:
| Name | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number, minimum 1. |
page_size | integer | 50 | Page size, minimum 1, maximum 200. |
sort_by | string | created_at_utc | One of created_at_utc, updated_at_utc, status, or external_contact_id. |
sort_order | string | asc | asc or desc. |
status | string | - | Contact status filter. |
disposition | string | - | Filters by last_disposition. |
callable | boolean | - | Filters by is_callable. |
validation_code | string | - | Filters by not_callable_reason. |
retry_due | boolean | - | true filters contacts whose next_attempt_at_utc is due. false is treated the same as omitted. |
external_contact_id | string | - | Filters by exact external contact id. |
In both listings, an unrecognized sort_by value falls back to the documented default sort column, and any sort_order other than asc is treated as desc.
#Request Bodies
#Create Campaign
POST /v1/outbound-campaigns accepts a JSON body with:
| Field | Type | Required | Description |
|---|---|---|---|
project_id | string | Yes | Project owned by the authenticated client. |
name | string | Yes | Campaign name, 1 to 255 characters. |
description | string or null | No | Optional description, maximum 4000 characters. |
campaign_type | string | No | single, batch, or scheduled; default is scheduled. |
campaign_call_type | string | Yes | telemarketing, research, or service. |
priority | integer | Yes | 1 to 100. |
schedule_start_at_utc | datetime or null | No | Optional UTC schedule start. |
schedule_end_at_utc | datetime or null | No | Optional UTC schedule end; must be after start when both are supplied. |
timezone | string | Yes | Campaign timezone name. |
retry_policy | object | Yes | Retry settings. |
voicemail_policy | object | Yes | Voicemail and IVR handling settings. |
personalization | object | Yes | Approved personalization keys. |
compliance | object | Yes | Campaign compliance settings. |
retry_policy contains enabled, max_attempts (0 to 20), backoff_base_seconds (0 to 604800), backoff_strategy (constant, linear, or exponential), and optional booleans retry_on_no_answer, retry_on_voicemail, and retry_on_busy.
voicemail_policy is the partner-facing name for the campaign's answering-machine handling policy. It contains detection_enabled, on_voicemail (leave_message, hangup, or retry), on_ivr (hangup or retry), on_silence (retry or hangup), on_unknown (proceed, retry, or hangup), and optional message_template. Create reads the current project runtime detector configuration from outbound_call.answering_machine_detection and persists the campaign snapshot into the current answering_machine_detection_* runtime columns. The old outbound project keys outbound_call.voicemail_detection and outbound_call.voicemail_handling are not accepted for campaign creation.
personalization.required_keys and personalization.allowed_keys must use keys of 64 characters or fewer containing only letters, numbers, hyphen, or underscore. Required keys must also be listed as allowed keys.
compliance contains registry_wash_required, registry_wash_max_age_days (1 to 365), consent_required, prevent_after_hours, check_public_holiday, recipient_timezone_aware, and optional CLI or exemption fields: cli_return_number, cli_employer_name, cli_authorizer_name, cli_call_purpose, and exemption_type.
Create loads the published project runtime configuration for the authenticated client. Missing, archived, unpublished, undeployed, or outbound-incomplete project runtime state returns a 422 validation response. A successful create returns 201 Created and the new campaign detail. Created campaigns start in draft.
#Update Campaign
PATCH /v1/outbound-campaigns/{campaign_id} accepts at least one of:
namedescriptionpriorityschedule_start_at_utcschedule_end_at_utc
Only draft, scheduled, and paused campaigns can be updated. Other states return 422 invalid_campaign_state.
#Registry Wash Record
POST /v1/outbound-campaigns/{campaign_id}/registry-wash-records accepts:
| Field | Type | Required | Description |
|---|---|---|---|
jurisdiction | string | Yes | Jurisdiction code, 2 to 10 characters. |
registry_type | string | Yes | Registry type, for example dncr. |
vendor | string or null | No | Registry vendor name. |
ruleset_id | string or null | No | Ruleset identifier. |
wash_channel | string | Yes | Wash channel. |
transaction_id | string or null | No | External transaction id. |
wash_file_name | string or null | No | External wash file name. |
numbers_submitted | integer | Yes | Total submitted numbers. |
numbers_on_register | integer | Yes | Count found on the registry. |
numbers_not_on_register | integer | Yes | Count not found on the registry. |
numbers_invalid | integer | Yes | Invalid-number count. |
credits_charged | integer | Yes | Registry credits charged. |
credits_remaining | integer or null | No | Remaining registry credits. |
washed_at_utc | datetime | Yes | Wash completion time. |
valid_until_utc | datetime | Yes | Evidence expiry time; must be after washed_at_utc. |
receipt_reference | string or null | No | External receipt reference. |
The three result counts must add up to numbers_submitted. Secret-like strings such as passwords, API keys, tokens, internal credential paths, or PEM markers are rejected in registry metadata fields. If a transaction_id already exists for the campaign, the latest matching record is returned instead of inserting a duplicate.
#CSV Import
POST /v1/outbound-campaigns/{campaign_id}/contacts/import accepts UTF-8 CSV, with an optional UTF-8 BOM, up to 1 MB and 5000 contact rows. Contacts can only be imported into draft or paused campaigns.
Required header:
to_number
Optional headers include:
external_contact_idrecipient_timezonerecipient_countryrecipient_subdivisionregistry_statusregistry_checked_at_utcregistry_valid_until_utcregistry_result_coderegistry_vendorregistry_ruleset_idconsent_typeconsent_sourceconsent_given_at_utcconsent_expires_at_utcconsent_withdrawn_at_utcconsent_evidence_referenceexemption_typeexemption_verifiedcall_typepriority_overridemax_attempts
Campaign-approved personalization keys may also be included as columns. Unsupported columns are rejected before row processing.
to_number must be E.164. If a non-empty external_contact_id occurs more than once, every occurrence in that request is rejected as a duplicate. For persisted rows, the API stores phone numbers as a hash plus server-side ciphertext, and stores personalization as server-side ciphertext plus a hash. Raw phone numbers and plaintext personalization are not returned.
Valid rows are accepted and invalid rows return row-level errors. A CSV with any row-level validation failures, including a file where every contact row is invalid, returns an import response with accepted and rejected row counts. Header-level, encoding, and size failures return 422.
#JSON Import
POST /v1/outbound-campaigns/{campaign_id}/contacts/import-json accepts a JSON object with:
| Field | Type | Required | Rules |
|---|---|---|---|
source_id | string | Yes | Trimmed non-PII operational identifier, maximum 128 characters; starts alphanumeric and then uses only letters, digits, ., _, :, or -. |
batch_id | string | Yes | Trimmed non-PII batch identifier, maximum 128 characters; the same safe-character rule applies. |
contacts | array | Yes | Between 1 and 5000 contact objects. |
Each contact uses the CSV field names listed above, with to_number and external_contact_id required by the JSON contract. Campaign-approved personalization keys may be added as flat fields. Every supplied contact value must be a JSON string; numbers, booleans, objects, arrays, and null are rejected without string coercion. Unsupported fields are rejected before persistence.
Every JSON external_contact_id must be a stable non-PII identifier. Blank or whitespace-only identifiers and identifiers shaped like an email address, phone number, or numeric phone value are rejected. Duplicate detection is a two-pass request check: when an identifier occurs more than once, no occurrence is accepted as the first row.
CSV and JSON imports use the same validation, encryption, accounting, and repository pipeline. Every completed non-replay import satisfies:
accepted_rows + duplicate_rows + rejected_rows = submitted contact rows
rejected_rows counts distinct rejected rows, not validation messages. Existing contacts identified by external_contact_id count as duplicates. Rolling updates are limited to pending, pending_dispatch, and queued contacts and preserve the current lifecycle state plus omitted registry, consent, exemption, recipient, personalization, and override evidence. A partial personalization update is merged server-side with the decrypted persisted payload, re-encrypted before persistence, and guarded by the previously observed content hash so a concurrent change is preserved rather than overwritten. Suppressed and terminal contacts are unchanged and cannot be resurrected.
Before an insert or permitted rolling update, the API checks the canonical phone hash for an existing direct_opt_out or wrong_number suppression in the same project and client. A match keeps the imported contact non-callable in dnc; contacts in another tenant or project do not affect the result.
Idempotency is scoped by the authenticated tenant plus a bounded hash of the authenticated data environment, resolved project, and campaign, together with the concrete endpoint and request key. The exact raw JSON body is hashed. A committed pending fingerprint prevents an in-flight request from being mistaken for a completed replay; a matching in-flight request returns 409 idempotency_request_in_progress. Replaying the same key and body after completion returns source_id, batch_id, refreshed safe campaign detail, and zero accepted, duplicate, and rejected rows without repeating mutations. Reusing the key with a changed body returns 422 idempotency_key_reuse. A failed import removes its pending reservation.
The API verifies a deterministic, non-secret phone-hash known answer during startup. Incompatible canonical hashing fails startup before imports or dispatch are served.
#Launch Validation
GET /v1/outbound-campaigns/{campaign_id}/validation and POST /v1/outbound-campaigns/{campaign_id}/validate compute the same launch validation result. Run and resume controls recompute validation before changing state. If validation is blocked, run and resume return the current campaign and validation result without transitioning the campaign.
Launch is blocked when required evidence or runtime prerequisites are missing, including:
- no imported contacts
- missing, stale, expired, or count-mismatched DNCR/registry wash evidence for telemarketing or research
- contacts on a registry, missing registry status, missing registry check timestamps, stale registry checks, or expired registry validity when enforcement is enabled
- missing, expired, withdrawn, or unverified consent or exemption evidence where consent is required
- missing or invalid recipient timezone, missing recipient location, after-hours, or public-holiday blockers for telemarketing or research campaigns governed by calling-window policy
- missing project runtime configuration or blocking project outbound compliance findings
- outbound capacity not configured
- missing CLI disclosure fields for telemarketing or research
Validation responses contain campaign_id, status (ready, blocked, or warning), validated_at_utc, blockers, warnings, info, and contact_blocker_counts. Each finding carries scope (campaign, contact, project, or capacity), code, field, and message. Findings are reported per blocker code, not per affected contact; per-contact totals are in contact_blocker_counts.
#Responses
Campaign list responses return:
| Field | Type | Description |
|---|---|---|
items | array | Campaign summaries. |
pagination | object | page, page_size, total, and total_pages. |
Campaign summaries include campaign_id, project_id, name, description, campaign_type, campaign_call_type, status, priority, timezone, counts, validation_state, schedule timestamps, registry-wash summary fields, and created/updated timestamps.
Campaign detail responses add started_at_utc, completed_at_utc, retry_policy, voicemail_policy, personalization, compliance, and nullable last_registry_wash_record_id.
Contact list responses return items and pagination. Contact items include contact_id, external_contact_id, masked_phone, personalization_keys, status, is_callable, not_callable_reason, registry summary fields, consent and exemption summary fields, attempt and retry timestamps, last_disposition, and nullable last_call_id. personalization_keys lists the campaign's allowed personalization keys; per-contact personalization keys and values are never returned.
Contact import responses include accepted_rows, rejected_rows, duplicate_rows, row_errors, and the refreshed campaign detail. JSON import responses also echo the safe source_id and batch_id.
Registry wash responses include registry_wash_record_id, campaign_id, jurisdiction, registry_type, wash_channel, transaction_id, submitted/on-register/not-on-register/invalid counts, washed_at_utc, and valid_until_utc.
Control responses include the campaign detail and, for run and resume, the launch validation result. Report responses include campaign_id, generated_at_utc, campaign-level aggregate counts by contact status, callable state, disposition, registry status, linked calls, retry-due total, empty voicemail_ivr_counts, and the current validation result.
#Outbound Evidence
#GET /v1/compliance/calls/{call_id}/outbound-evidence
Returns the safe outbound-compliance evidence summary for a scoped call.
Every response carries call_id, project_id, and evidence_status (recorded, not_recorded, or not_outbound). A recorded response adds policy_source, policy_version, call_type, jurisdiction, and the timezone_evaluation (calling-window), holiday_evaluation, dncr_summary, cli_summary, and retry_summary blocks. Consent and exemption evidence is reported as consent_status and exemption_status inside dncr_summary. Unset fields are omitted. Inbound calls return not_outbound, and calls whose stored evidence is missing or carries unexpected fields return not_recorded with no summary blocks.
This route has its own read scopes: partner_admin, partner_user, aidial_admin, and aidial_operator for API keys, plus client_admin, client_manager, and client_staff for bearer sessions. Bearer partner principals also require an active assignment for the call's project. Out-of-scope callers and unknown calls return 404.
It never returns raw phone numbers, imported CSV content, transcript text, recordings, ciphertext, API keys, bearer tokens, or provider credentials.
#Response Safety
Responses return only safe summaries and campaign configuration fields:
- masked contact phone placeholders. The current contact list uses
**** - contact status and outcome metadata
- registry, consent, and exemption summaries
- campaign counts and validation findings
- linked-call aggregate counts
- configured campaign compliance fields, including CLI disclosure values
The API never returns raw imported CSV content, decrypted imported contact phone numbers, plaintext personalization, encrypted database fields, credentials, or raw runtime configuration.
#Error Semantics
Outbound campaign domain errors use the partner error envelope from core.error_envelope, except framework-level authentication and validation errors which use the shared API error shape.
| Condition | Status |
|---|---|
| Missing, invalid, revoked, or expired authentication | 401 Unauthorized |
| Valid bearer token with no active supported assignment | 404 Not Found |
| Role not allowed, out-of-scope campaign, missing campaign, or invalid campaign id | 404 Not Found |
Missing or invalid Idempotency-Key on required mutation routes | 400 Bad Request |
| FastAPI request-shape validation failure | 422 Unprocessable Entity |
| Campaign, CSV, JSON-contact, registry, runtime-readiness, or state validation failure | 422 Unprocessable Entity |
Duplicate Idempotency-Key with a different request body | 422 Unprocessable Entity |
| Registry idempotency replay cannot find a completed prior record | 409 Conflict |
Missing (client_id, data_env) route in the tenant routing map | 500 Internal Server Error |
#Deletion Policy
There is no delete endpoint. Use POST /v1/outbound-campaigns/{campaign_id}/cancel to stop a campaign while preserving audit and reporting history.