ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Calls

Campaigns

Run outbound calling campaigns: upload contacts, set calling windows, and track outcomes.

A campaign dials a list of contacts with one of your agents, from one of your outbound-capable numbers, inside a daily calling window in the timezone you pick. The runner respects a per-campaign concurrency cap and retries contacts whose calls ended in outcomes you selected (no_answer, voicemail, failed, …) with a configurable backoff, up to max_attempts per contact.

Lifecycle: draft → (scheduled →) runningcompleted, with paused and cancelled reachable via actions. Contacts are only editable while the campaign is a draft.

Endpoints

MethodPathDescription
GET/v1/campaignsList campaigns
POST/v1/campaignsCreate a campaign (starts in draft)
GET/v1/campaigns/{campaign_id}Retrieve a campaign incl. contacts
PATCH/v1/campaigns/{campaign_id}Update a draft/scheduled/paused campaign
DELETE/v1/campaigns/{campaign_id}Delete a campaign
POST/v1/campaigns/{campaign_id}/contactsAdd contacts (CSV upload or JSON)
GET/v1/campaigns/{campaign_id}/statsProgress counters + recent calls
POST/v1/campaigns/{campaign_id}/{action}start, pause, or cancel

{campaign_id} is the campaign's UUID (public_id).

Campaign object

{
  "public_id": "3f6b2c9e-…",
  "name": "May win-back",
  "agent": 12,
  "agent_name": "Customer Support Agent",
  "from_number": "+15551234567",
  "status": "draft",
  "timezone": "America/Los_Angeles",
  "daily_window_start": "09:00:00",
  "daily_window_end": "17:00:00",
  "start_at": null,
  "end_at": null,
  "max_concurrent_calls": 3,
  "max_attempts": 2,
  "retry_backoff_minutes": 60,
  "retry_on": ["no_answer", "voicemail", "failed"],
  "total_count": 0,
  "completed_count": 0,
  "failed_count": 0,
  "in_progress_count": 0,
  "created_at": "2026-04-20T18:24:10.113Z",
  "updated_at": "2026-04-20T18:24:10.113Z"
}
FieldTypeDescription
public_idUUIDCampaign id (used in every path)
namestring1–255 chars
agentintegerThe Agent that speaks on every call (writable)
agent_namestringRead-only convenience
from_numberstringAn outbound-capable number in your org. Demo numbers are rejected
statusstringdraft, scheduled, running, paused, completed, cancelled (read-only — change via actions)
timezonestringIANA timezone the daily window is evaluated in. Default UTC
daily_window_start, daily_window_endtimeLocal calling window, e.g. "09:00:00""17:00:00"
start_at, end_attimestamp | nullOptional overall schedule bounds. end_at must be after start_at
max_concurrent_callsinteger1–10, default 3
max_attemptsinteger≥ 1, default 2 — total tries per contact
retry_backoff_minutesintegerDelay before a retryable contact is attempted again. Default 60
retry_onarray of stringCall outcomes that trigger a retry. Default ["no_answer", "voicemail", "failed"]
total_count, completed_count, failed_count, in_progress_countintegerProgress counters (read-only)

Create a campaign

cURL
curl -X POST https://api.thunderphone.com/v1/campaigns \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "May win-back",
    "agent": 12,
    "from_number": "+15551234567",
    "timezone": "America/Los_Angeles",
    "daily_window_start": "09:00",
    "daily_window_end": "17:00",
    "max_concurrent_calls": 3,
    "max_attempts": 2,
    "retry_on": ["no_answer", "voicemail"]
  }'

Required: name, agent, from_number, daily_window_start, daily_window_end. Returns 201 Created with the campaign in status="draft". Validation errors (400) cover unknown agents, numbers not registered to your org, demo numbers, invalid IANA timezones, and end_at <= start_at.

Retrieve / update / delete

GET /v1/campaigns/{id} returns the campaign plus a contacts array (first 500 contact objects; filter with ?contact_status=pending|scheduled|calling|completed|failed|exhausted).

PATCH accepts any subset of the writable fields, but only while status is draft, scheduled, or paused — otherwise 409 Conflict.

DELETE returns 204 No Content and removes the campaign and its contacts (already-made call logs are kept).


Add contacts

Only allowed while the campaign is a draft (409 otherwise). Two formats:

CSV upload
curl -X POST https://api.thunderphone.com/v1/campaigns/3f6b2c9e-…/contacts \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -F 'file=@contacts.csv'
JSON
curl -X POST https://api.thunderphone.com/v1/campaigns/3f6b2c9e-…/contacts \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      { "phone_number": "+14155550199", "first_name": "Jamie", "account_id": "A-1042" }
    ]
  }'
  • CSV: UTF-8, must have a phone or phone_number column. Every other column becomes a per-contact variable.
  • JSON: a top-level list, or {"contacts": [...]} — each object needs phone_number (or phone); all other keys become variables.
  • Max 5,000 rows per request. Numbers are normalized to E.164 (8–15 digits); invalid rows are rejected individually, not the whole batch.
Response (201 Created)
{
  "accepted": 148,
  "rejected": 2,
  "errors": [
    { "row": 17, "error": "Enter a valid E.164 phone number (8-15 digits)." }
  ]
}

Contact object

{id, phone_number, variables, status, attempts, next_attempt_at, last_outcome, call_id, created_at, updated_at}status is pending, scheduled, calling, completed, failed, or exhausted (retries used up); call_id links the most recent call.


Campaign actions

POST /v1/campaigns/{id}/{action} where action is:

ActionBodyEffect
start{"consent_to_charge": true} (required)draft/paused/scheduled/cancelledrunning (or scheduled when start_at is in the future). Requires at least one contact and the org's outbound TCPA confirmation
pauserunning/scheduledpaused
cancelAny non-terminal status → cancelled

Returns 200 OK with the updated campaign. Invalid transitions return 409; unknown actions return 404.


Stats

cURL
curl https://api.thunderphone.com/v1/campaigns/3f6b2c9e-…/stats \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Response
{
  "total": 150,
  "completed": 41,
  "failed": 3,
  "in_progress": 2,
  "by_status": { "pending": 96, "scheduled": 8, "calling": 2, "completed": 41, "failed": 3 },
  "recent_calls": [
    {
      "contact_id": 9911,
      "phone_number": "+14155550199",
      "call_id": 987654321,
      "status": "completed",
      "outcome": "completed",
      "started_at": "2026-04-20T18:24:10.113Z"
    }
  ]
}

recent_calls holds the 20 most recently updated contacts that have a call attached.