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 your organization's call limits (a contact that meets a limit waits for a later pass without using an attempt), and retries contacts whose calls ended in outcomes you selected (no_answer, voicemail, failed, …) with a configurable backoff, up to max_attempts per contact. See calling hours and retry limits for the timezone default, the window warnings, and the retry bounds.

Lifecycle: draft → (scheduled →) running → completed, 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,
  "warnings": [],
  "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. ThunderPhone numbers are accepted after outbound calling is approved for your organization
statusstringdraft, scheduled, running, paused, completed, cancelled (read-only — change via actions)
timezonestringIANA timezone the daily window is evaluated in. When omitted on create, it defaults to the from_number's zone (its area code in the US and Canada, its country elsewhere), else UTC
daily_window_start, daily_window_endtimeLocal calling window, e.g. "09:00:00"–"17:00:00". Calls start from daily_window_start up to, not at, daily_window_end. On create, send both or neither; neither means "09:00:00"–"17:00:00". An end earlier than the start makes an overnight window ("22:00:00"–"02:00:00" dials from 10pm until 2am the next day)
start_at, end_attimestamp | nullOptional overall schedule bounds. end_at must be after start_at
max_concurrent_callsinteger1–10, default 3
max_attemptsinteger1–10, default 2 — total tries per contact. The upper bound is per organization and staff can raise it
retry_backoff_minutesintegerDelay before a retryable contact is attempted again. At least 30, default 60. Staff can lower the minimum per organization
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)
warningsarray of objectSaved settings worth a second look, each {code, field, detail} (read-only). outside_calling_hours for a window that starts before 08:00 (field is daily_window_start) or ends after 21:00 or crosses midnight (daily_window_end), one entry per edge. window_crosses_midnight for an overnight window and empty_window for equal start and end, which does not dial. Empty when there is nothing to flag

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, ThunderPhone numbers when outbound calling has not been approved for your organization, invalid IANA timezones, end_at <= start_at, and retry settings outside your organization's bounds (below). Daily windows are never refused; unusual ones come back with warnings.

Calling hours and retry limits

  • Timezone default. Omit timezone and the campaign uses the from_number's zone: its area code for US and Canadian numbers (+1 415… is America/Los_Angeles), its country elsewhere. Numbers that span several zones, such as toll-free numbers, fall back to UTC. The same value is on the phone number object as timezone. A timezone you send always wins.
  • Calling-hours warning. Calls outside 8am–9pm local time may break calling-hours rules such as the TCPA's. A window that starts before 08:00 or ends after 21:00 is saved, and the response lists an outside_calling_hours entry in warnings. The window runs in the campaign's timezone, not each contact's: a 9am start in New York reaches a Los Angeles contact at 6am.
  • Overnight windows. A daily_window_end earlier than daily_window_start crosses midnight: 22:00–02:00 dials from 10pm until 2am the next day. It is saved with window_crosses_midnight and outside_calling_hours warnings, for example when your contacts sit in a timezone hours away from the campaign's. Equal start and end (09:00–09:00) is saved with an empty_window warning and never dials.
  • Retry bounds. max_attempts is at most 10 and retry_backoff_minutes at least 30. Values outside them return 400 on that field. Staff can raise both for your organization; contact support.
400 Bad Request
{
  "max_attempts": ["Campaigns can make at most 10 attempts per contact. Contact support to raise this limit."]
}

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.

Campaigns saved before the retry bounds keep running exactly as configured. Sending a max_attempts or retry_backoff_minutes outside the bounds is refused only when it changes the saved value, so other edits still succeed.

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": "+14155552671", "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.
  • Rows with a number ThunderPhone does not call are rejected the same way, under the outbound call rules: emergency numbers, short codes, sanctioned countries, and, for a ThunderPhone caller ID, premium-rate numbers and anything but ordinary US and Canadian numbers. The row's reason field names the rule. A contact that reaches dial time with such a number fails with last_outcome destination_not_allowed and is not retried.
Response (201 Created)
{
  "accepted": 148,
  "rejected": 2,
  "errors": [
    { "row": 17, "value": "555-0100", "error": "\"555-0100\" isn't a valid phone number. Check the digits and the country code.", "reason": "invalid_number" },
    { "row": 42, "value": "", "error": "Expected an object." }
  ],
  "errors_truncated": false
}

errors carries at most 100 entries; errors_truncated is true when more rows were rejected than listed. value is the phone value of the rejected row (empty when the row was not an object).

Contact object

{id, phone_number, variables, status, attempts, next_attempt_at, last_outcome, call_id, dial_sip_status, dial_sip_reason, dial_sip_kind, created_at, updated_at} — status is pending, scheduled, calling, completed, failed, or exhausted (retries used up); call_id links the most recent call. dial_sip_status, dial_sip_reason and dial_sip_kind are that call's outbound_dial_sip_status, outbound_dial_sip_reason and outbound_dial_sip_kind (e.g. 403, Account is disabled D17 and refused when your carrier refused the dial); all are null when there is no call or no carrier answered it.


Campaign actions

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

ActionBodyEffect
start{"consent_to_charge": true} (required)draft/paused/scheduled/cancelled → running (or scheduled when start_at is in the future). Requires at least one contact and the org's outbound TCPA confirmation
pause—running/scheduled → paused
cancel—Any 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": "+14155552671",
      "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.