zoxaAI
Homepage
API ReferenceCampaigns

Create a campaign

POST /api/v1/campaign/create — create a draft campaign targeting an agent.

POST /api/v1/campaign/create

Create 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

FieldTypeRequiredNotes
namestring (1..255)✓Display name.
agent_uuidstring✓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_idstring✓CSV file storage key (e.g., from Files).
telephony_configuration_idint—Telephony configuration to dial from. If omitted, the org's default telephony config is used and persisted on the campaign row.
max_concurrencyint (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_configobject—See below.
schedule_configobject—See below.
circuit_breakerobject—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
}
FieldTypeDefaultRange
enabledbooltrue—
max_retriesint20..10
retry_delay_secondsint12010..3600
retry_on_busybooltrue—
retry_on_no_answerbooltrue—
retry_on_voicemailbooltrue—
backoff_multiplierfloat1.01.0..5.0 — 1.0 keeps a fixed delay; > 1.0 applies exponential backoff.
retry_delay_cap_secondsint360060..7200 — hard cap on any computed delay.
busy_delay_secondsint | nullnull10..3600 — per-reason base delay for busy; falls back to retry_delay_seconds.
no_answer_delay_secondsint | nullnull10..3600 — per-reason base delay for no-answer.
voicemail_delay_secondsint | nullnull10..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" }
  ]
}
FieldTypeDefaultNotes
enabledbooltrueWhen false, the schedule is stored but dispatching ignores it.
timezonestring"UTC"IANA timezone (e.g. "America/New_York"). Validated via zoneinfo.
slotsarray (1..50)—At least one slot required.
slots[].day_of_weekint (0..6)—Day index — 0 = Monday in this codebase.
slots[].start_timestring HH:MM—24-hour format. Regex ^\d{2}:\d{2}$.
slots[].end_timestring 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
}
FieldTypeDefaultRange
enabledbooltrue—
failure_thresholdfloat0.50.0..1.0 — fraction of failed calls that trips the breaker.
window_secondsint12030..600 — rolling evaluation window.
min_calls_in_windowint51..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

StatusdetailWhen
400source validation messageCSV 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.
422arraySchema validation — source_type must be "csv", missing agent_uuid, malformed slots[].start_time, unknown timezone, end_time <= start_time, etc.

On this page