Create a campaign
POST /api/v1/campaign/create — create a draft campaign targeting an agent.
POST /api/v1/campaign/createCreate a draft campaign. The contact list is validated up-front — if the CSV is missing a phone_number column or is malformed, the request fails before the campaign is saved.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string (1..255) | ✓ | Display name. |
agent_uuid | string | ✓ | Target agent — its public uuid from GET /agents (resolved org-scoped to the internal id server-side). |
source_type | "csv" | ✓ | Only "csv" is accepted (regex-validated to ^csv$). |
source_id | string | ✓ | CSV file storage key (e.g., from Files). |
telephony_configuration_id | int | — | Telephony configuration to dial from. If omitted, the org's default telephony config is used and persisted on the campaign row. |
max_concurrency | int (1..100) | — | Cap on simultaneously-active calls. Further capped at the lower of the org ceiling and the available caller-number count (that count comes from the org's default telephony config). |
retry_config | object | — | See below. |
schedule_config | object | — | See below. |
circuit_breaker | object | — | See below. |
Telephony configuration scopes the from-number pool
The outbound caller-ID pool is keyed per (organization, telephony_configuration_id). Two campaigns running on different telephony configs draw from independent pools and never share numbers.
retry_config
{
"enabled": true,
"max_retries": 2,
"retry_delay_seconds": 120,
"retry_on_busy": true,
"retry_on_no_answer": true,
"retry_on_voicemail": true,
"backoff_multiplier": 1.0,
"retry_delay_cap_seconds": 3600,
"busy_delay_seconds": null,
"no_answer_delay_seconds": null,
"voicemail_delay_seconds": null
}| Field | Type | Default | Range |
|---|---|---|---|
enabled | bool | true | — |
max_retries | int | 2 | 0..10 |
retry_delay_seconds | int | 120 | 10..3600 |
retry_on_busy | bool | true | — |
retry_on_no_answer | bool | true | — |
retry_on_voicemail | bool | true | — |
backoff_multiplier | float | 1.0 | 1.0..5.0 — 1.0 keeps a fixed delay; > 1.0 applies exponential backoff. |
retry_delay_cap_seconds | int | 3600 | 60..7200 — hard cap on any computed delay. |
busy_delay_seconds | int | null | null | 10..3600 — per-reason base delay for busy; falls back to retry_delay_seconds. |
no_answer_delay_seconds | int | null | null | 10..3600 — per-reason base delay for no-answer. |
voicemail_delay_seconds | int | null | null | 10..3600 — per-reason base delay for voicemail. |
The delay for retry attempt N (1-based) is base × backoff_multiplier^(N-1), where base is the per-reason override if set, else retry_delay_seconds. When backoff_multiplier > 1.0, ±20% jitter is applied. The result is capped at retry_delay_cap_seconds. See the platform guide for a worked example.
Voicemail retries
retry_on_voicemail relies on the agent's voicemail detection — the pipeline detects the answering machine on the first turn and schedules a retry. Agent voicemail detection is on by default and can be disabled per agent in the agent editor's Call settings.
schedule_config
Schedule by time slots per day of week. The campaign only dispatches during these slots.
{
"enabled": true,
"timezone": "Asia/Kolkata",
"slots": [
{ "day_of_week": 0, "start_time": "09:00", "end_time": "18:00" },
{ "day_of_week": 1, "start_time": "09:00", "end_time": "18:00" }
]
}| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | When false, the schedule is stored but dispatching ignores it. |
timezone | string | "UTC" | IANA timezone (e.g. "America/New_York"). Validated via zoneinfo. |
slots | array (1..50) | — | At least one slot required. |
slots[].day_of_week | int (0..6) | — | Day index — 0 = Monday in this codebase. |
slots[].start_time | string HH:MM | — | 24-hour format. Regex ^\d{2}:\d{2}$. |
slots[].end_time | string HH:MM | — | Must be strictly greater than start_time (string comparison on HH:MM works for same-day slots). |
circuit_breaker
Auto-pauses the campaign if the failure rate spikes within a rolling window.
{
"enabled": true,
"failure_threshold": 0.5,
"window_seconds": 120,
"min_calls_in_window": 5
}| Field | Type | Default | Range |
|---|---|---|---|
enabled | bool | true | — |
failure_threshold | float | 0.5 | 0.0..1.0 — fraction of failed calls that trips the breaker. |
window_seconds | int | 120 | 30..600 — rolling evaluation window. |
min_calls_in_window | int | 5 | 1..100 — minimum calls in the window before the threshold is evaluated. |
Response
200 OK. State starts at created.
{
"id": 7,
"name": "Q3 outreach",
"state": "created",
"agent_id": 42,
"agent_name": "Sales bot",
"source_type": "csv",
"source_id": "campaign_sources/org_1/contacts.csv",
"telephony_configuration_id": 3,
"telephony_configuration_name": "Twilio · main",
"total_rows": null,
"processed_rows": 0,
"failed_rows": 0,
"max_concurrency": 5,
"retry_config": { /* echo of input or defaults */ },
"schedule_config": { /* echo */ },
"circuit_breaker": { /* echo or defaults */ },
"executed_count": 0,
"total_queued_count": 0,
"logs": [],
"created_at": "2026-06-16T10:00:00Z",
"started_at": null,
"completed_at": null
}You send agent_uuid in the request, but the response echoes the resolved agent as its internal agent_id (int) plus agent_name — there is no agent_uuid field on the campaign response.
The campaign response uses state, not status. Values from the campaign_state Postgres enum: created (default — campaign created, not started), syncing (importing rows from the CSV), running, paused, completed, failed. See Get a campaign for the full field list, including the structured logs[] array.
Examples
curl -X POST https://dashboard.zoxa.ai/api/v1/campaign/create \
-H "X-API-Key: zsk_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Q3 outreach",
"agent_uuid": "550e8400-e29b-41d4-a716-446655440000",
"source_type": "csv",
"source_id": "campaign_sources/org_1/contacts.csv",
"max_concurrency": 5,
"schedule_config": {
"enabled": true,
"timezone": "Asia/Kolkata",
"slots": [
{"day_of_week": 0, "start_time": "09:00", "end_time": "18:00"},
{"day_of_week": 1, "start_time": "09:00", "end_time": "18:00"},
{"day_of_week": 2, "start_time": "09:00", "end_time": "18:00"},
{"day_of_week": 3, "start_time": "09:00", "end_time": "18:00"},
{"day_of_week": 4, "start_time": "09:00", "end_time": "18:00"}
]
}
}'Errors
| Status | detail | When |
|---|---|---|
400 | source validation message | CSV missing phone_number column, malformed file, etc. |
400 | "max_concurrency (X) cannot exceed Y. You have N phone number(s)..." | Above the campaign telephony config's phone-number count. |
400 | "max_concurrency (X) cannot exceed organization limit (Y)" | Above the org's overall ceiling. |
400 | "telephony_configuration_not_found" | The telephony_configuration_id you passed doesn't exist or doesn't belong to your org. |
404 | "Agent not found" | Target doesn't exist (or doesn't belong to your org). |
409 | "no telephony configuration available for organization" | You omitted telephony_configuration_id and your org has no default telephony config. |
422 | array | Schema validation — source_type must be "csv", missing agent_uuid, malformed slots[].start_time, unknown timezone, end_time <= start_time, etc. |
Related
POST /campaign/{id}/start— begin dispatchingGET /campaign/{id}/progress— watch live