curl, from CSV to downloaded report.
Prefer clicking? Run a calling campaign covers this same workflow from the dashboard’s Batches screen — no
curl required. This page drives the same endpoints over HTTP, which is what you want for scripting and scheduling. See also Dashboard tour.Before you start
Four things must exist beforePOST /api/v1/campaign/create will succeed. The API checks each one and returns a specific error if it is missing.
The organisation limit comes from
DEFAULT_ORG_CONCURRENCY_LIMIT, default 10, clamped to a minimum of 1 in apps/api/app/constants/campaign.py.
Set a token and the API base once:
The contact CSV
The CSV contract is enforced byCampaignSourceSyncService.validate_source_data in apps/api/app/services/campaign/source_sync.py.
Rows with a blank
phone_number are skipped at sync time rather than rejected at validation time.
customer_name and account_id reach the agent as call-time variables. Declare matching keys in the agent’s config.custom_variables so the prompt can reference them — see Agent configuration.
Upload
Upload is a multipart POST to/api/v1/campaign/upload. It stores the file in MinIO and validates it in one call.
contact_rows counts data rows that carry a non-empty phone_number. Keep source_id — it is the MinIO object key you pass to create.
The path is
/api/v1/campaign/upload. There is no /upload-csv route.Create the campaign
created. It does not dial until you start it. Full field bounds and defaults are in Campaigns.
Retry, schedule, concurrency and circuit breaker
max_concurrency, schedule_config, and circuit_breaker are request fields at the top level of the create body, but the API nests them inside orchestrator_metadata on the stored document. Read them back from orchestrator_metadata, not from the top level of the response. Only retry_config is stored as its own top-level field.Retry
retry_config governs whether a failed contact is queued again. Field bounds and defaults are in Campaigns.
A retry is a new queued run with retry_count incremented and is_retry, retry_attempt, and retry_reason merged into its context variables, so the prompt can tell a redial from a first attempt.
Schedule
schedule_config confines dialling to weekly windows. When enabled is false or slots is empty, the campaign dials at any hour.
day_of_week is 0 for Monday through 6 for Sunday. Times are HH:MM and compared as strings against the current time in timezone, with the window half-open — start_time inclusive, end_time exclusive. An unrecognised timezone falls back to “always in schedule” rather than failing.
Outside a window the orchestrator declines to schedule the next batch. The campaign stays running and resumes at the next window without any action from you.
Concurrency
max_concurrency is this campaign’s ceiling on simultaneous live calls, enforced by the dispatcher as a campaign:{campaign_id} scope on top of the organisation’s slot pool. rate_limit_per_second throttles how fast new calls are placed. See Call concurrency and rate limiting.
The orchestrator dispatches in batches of CAMPAIGN_BATCH_SIZE, default 10, set on the campaign-orchestrator service in docker-compose.yaml.
Circuit breaker
The breaker pauses a campaign that is failing broadly, so a bad number range or a dead provider does not burn the whole list. Field bounds and defaults are in Campaigns. The counts live in Redis sorted sets keyed per campaign and are evaluated in a Lua script, so the check is atomic across workers. A full create body with every block:PATCH /api/v1/campaign/{campaign_id} accepts the same blocks and merges them into orchestrator_metadata:
Start, pause and resume
The three lifecycle routes are allPOST with no body. Each rejects a campaign in the wrong state with 400.
A redial child campaign skips the sync step:
start_campaign sees parent_campaign_id in its metadata, goes straight to running, and publishes sync-completed itself, because its runs were created at redial time.
Watching progress
Two routes.progress is the summary; runs is the per-call detail.
progress_percentage is processed_rows / total_rows * 100, and 0 when total_rows is 0 — which is what you see between /start and the end of the source sync.
runs returns CallLog documents for the campaign, limit 1–500 (default 50) and offset from 0. Artifact URLs are rewritten to the authenticated API proxy routes, so a call’s recording and transcript are fetched with GET /api/v1/calls/{call_id}/recording and .../transcript.
Poll progress on an interval; there is no push channel exposed to API clients. The Redis campaign event bus is internal to the orchestrator and worker.
Listing and fetching campaigns:
Reading the report
/report streams CSV, not JSON. It is capped at the 500 most recent call logs for the campaign.
call_id, to_number, status, call_response, duration, created_at.
For a campaign longer than 500 calls, page /runs instead and build your own file.
To get the original contact CSV back, ask for a presigned MinIO link:
Redial
Redial creates a new child campaign over the failed contacts of a finished one. It does not modify the parent.{"name": "..."} and nothing else. The child inherits the parent’s agent_id, source_type, source_id, rate_limit_per_second, retry_config, from_number, and whole orchestrator_metadata, with parent_campaign_id added. Queued runs are built from the parent’s redial candidates, carrying their original context_variables, and total_rows is set to that count.
The child is returned in state created. Start it explicitly — redial does not dial anything on its own:
400 No failed contacts to redial means there is nothing to retry.
When a campaign halts itself
A campaign can stop moving without you touching it. Four causes, distinguished by state andprogress.
The breaker is checked from two directions: once per call outcome as the result lands, and again before each batch is scheduled. Both paths set the campaign to
paused and publish the same event, so a tripped breaker cannot be missed by an idle campaign.
Completion is inferred, not asserted. _should_mark_complete requires no batch in progress, no queued or processing runs, and at least completion_timeout (3600 seconds) since the last activity — from in-memory state, or from last_activity_at, last_batch_scheduled_at, or started_at on the document if the orchestrator restarted. The check runs every 60 seconds.