#Outbound Campaigns

12 min read

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:

  1. X-API-Key
  2. Authorization: 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

EndpointMethodScopePurpose
/v1/outbound-campaignsGETreadList campaign summaries with project, status, call type, and search filters.
/v1/outbound-campaignsPOSTpartner_adminCreate a campaign.
/v1/outbound-campaigns/{campaign_id}GETreadRead safe campaign detail.
/v1/outbound-campaigns/{campaign_id}PATCHpartner_adminUpdate mutable campaign metadata while state allows edits.
/v1/outbound-campaigns/{campaign_id}/contactsGETreadList masked contact status, outcome, retry, consent, and registry summaries.
/v1/outbound-campaigns/{campaign_id}/contacts/importPOSTpartner_adminImport CSV contacts. Requires Idempotency-Key.
/v1/outbound-campaigns/{campaign_id}/contacts/import-jsonPOSTpartner_adminImport a bounded JSON contact batch. Requires Idempotency-Key.
/v1/outbound-campaigns/{campaign_id}/registry-wash-recordsPOSTpartner_adminAttach DNCR/registry wash evidence. Requires Idempotency-Key.
/v1/outbound-campaigns/{campaign_id}/validationGETreadRead launch validation state.
/v1/outbound-campaigns/{campaign_id}/validatePOSTpartner_adminRecompute and audit launch validation.
/v1/outbound-campaigns/{campaign_id}/runPOSTpartner_adminLaunch after validation passes. Requires Idempotency-Key.
/v1/outbound-campaigns/{campaign_id}/pausePOSTpartner_adminPause campaign dispatch. Requires Idempotency-Key.
/v1/outbound-campaigns/{campaign_id}/resumePOSTpartner_adminResume after validation passes. Requires Idempotency-Key.
/v1/outbound-campaigns/{campaign_id}/cancelPOSTpartner_adminCancel without deleting history. Requires Idempotency-Key.
/v1/outbound-campaigns/{campaign_id}/reportGETreadRead reporting aggregates and validation state.

#Query Parameters

GET /v1/outbound-campaigns supports:

NameTypeDefaultDescription
pageinteger1Page number, minimum 1.
page_sizeinteger20Page size, minimum 1, maximum 100.
sort_bystringupdated_at_utcOne of updated_at_utc, created_at_utc, name, or status.
sort_orderstringdescasc or desc.
project_idstring-Filters campaigns to one project within the authenticated client scope.
statusstring-Filters by campaign status.
campaign_call_typestring-Filters by telemarketing, research, or service.
searchstring-Case-insensitive campaign-name search, maximum 120 characters.

GET /v1/outbound-campaigns/{campaign_id}/contacts supports:

NameTypeDefaultDescription
pageinteger1Page number, minimum 1.
page_sizeinteger50Page size, minimum 1, maximum 200.
sort_bystringcreated_at_utcOne of created_at_utc, updated_at_utc, status, or external_contact_id.
sort_orderstringascasc or desc.
statusstring-Contact status filter.
dispositionstring-Filters by last_disposition.
callableboolean-Filters by is_callable.
validation_codestring-Filters by not_callable_reason.
retry_dueboolean-true filters contacts whose next_attempt_at_utc is due. false is treated the same as omitted.
external_contact_idstring-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:

FieldTypeRequiredDescription
project_idstringYesProject owned by the authenticated client.
namestringYesCampaign name, 1 to 255 characters.
descriptionstring or nullNoOptional description, maximum 4000 characters.
campaign_typestringNosingle, batch, or scheduled; default is scheduled.
campaign_call_typestringYestelemarketing, research, or service.
priorityintegerYes1 to 100.
schedule_start_at_utcdatetime or nullNoOptional UTC schedule start.
schedule_end_at_utcdatetime or nullNoOptional UTC schedule end; must be after start when both are supplied.
timezonestringYesCampaign timezone name.
retry_policyobjectYesRetry settings.
voicemail_policyobjectYesVoicemail and IVR handling settings.
personalizationobjectYesApproved personalization keys.
complianceobjectYesCampaign 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:

  • name
  • description
  • priority
  • schedule_start_at_utc
  • schedule_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:

FieldTypeRequiredDescription
jurisdictionstringYesJurisdiction code, 2 to 10 characters.
registry_typestringYesRegistry type, for example dncr.
vendorstring or nullNoRegistry vendor name.
ruleset_idstring or nullNoRuleset identifier.
wash_channelstringYesWash channel.
transaction_idstring or nullNoExternal transaction id.
wash_file_namestring or nullNoExternal wash file name.
numbers_submittedintegerYesTotal submitted numbers.
numbers_on_registerintegerYesCount found on the registry.
numbers_not_on_registerintegerYesCount not found on the registry.
numbers_invalidintegerYesInvalid-number count.
credits_chargedintegerYesRegistry credits charged.
credits_remaininginteger or nullNoRemaining registry credits.
washed_at_utcdatetimeYesWash completion time.
valid_until_utcdatetimeYesEvidence expiry time; must be after washed_at_utc.
receipt_referencestring or nullNoExternal 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_id
  • recipient_timezone
  • recipient_country
  • recipient_subdivision
  • registry_status
  • registry_checked_at_utc
  • registry_valid_until_utc
  • registry_result_code
  • registry_vendor
  • registry_ruleset_id
  • consent_type
  • consent_source
  • consent_given_at_utc
  • consent_expires_at_utc
  • consent_withdrawn_at_utc
  • consent_evidence_reference
  • exemption_type
  • exemption_verified
  • call_type
  • priority_override
  • max_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:

FieldTypeRequiredRules
source_idstringYesTrimmed non-PII operational identifier, maximum 128 characters; starts alphanumeric and then uses only letters, digits, ., _, :, or -.
batch_idstringYesTrimmed non-PII batch identifier, maximum 128 characters; the same safe-character rule applies.
contactsarrayYesBetween 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:

FieldTypeDescription
itemsarrayCampaign summaries.
paginationobjectpage, 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.

ConditionStatus
Missing, invalid, revoked, or expired authentication401 Unauthorized
Valid bearer token with no active supported assignment404 Not Found
Role not allowed, out-of-scope campaign, missing campaign, or invalid campaign id404 Not Found
Missing or invalid Idempotency-Key on required mutation routes400 Bad Request
FastAPI request-shape validation failure422 Unprocessable Entity
Campaign, CSV, JSON-contact, registry, runtime-readiness, or state validation failure422 Unprocessable Entity
Duplicate Idempotency-Key with a different request body422 Unprocessable Entity
Registry idempotency replay cannot find a completed prior record409 Conflict
Missing (client_id, data_env) route in the tenant routing map500 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.