Open in
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
| Method | Path | Description |
|---|---|---|
GET | /v1/campaigns | List campaigns |
POST | /v1/campaigns | Create 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}/contacts | Add contacts (CSV upload or JSON) |
GET | /v1/campaigns/{campaign_id}/stats | Progress 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"
}| Field | Type | Description |
|---|---|---|
public_id | UUID | Campaign id (used in every path) |
name | string | 1–255 chars |
agent | integer | The Agent that speaks on every call (writable) |
agent_name | string | Read-only convenience |
from_number | string | An outbound-capable number in your org. ThunderPhone numbers are accepted after outbound calling is approved for your organization |
status | string | draft, scheduled, running, paused, completed, cancelled (read-only — change via actions) |
timezone | string | IANA 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_end | time | Local 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_at | timestamp | null | Optional overall schedule bounds. end_at must be after start_at |
max_concurrent_calls | integer | 1–10, default 3 |
max_attempts | integer | 1–10, default 2 — total tries per contact. The upper bound is per organization and staff can raise it |
retry_backoff_minutes | integer | Delay before a retryable contact is attempted again. At least 30, default 60. Staff can lower the minimum per organization |
retry_on | array of string | Call outcomes that trigger a retry. Default ["no_answer", "voicemail", "failed"] |
total_count, completed_count, failed_count, in_progress_count | integer | Progress counters (read-only) |
warnings | array of object | Saved 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 -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
timezoneand the campaign uses thefrom_number's zone: its area code for US and Canadian numbers (+1 415…isAmerica/Los_Angeles), its country elsewhere. Numbers that span several zones, such as toll-free numbers, fall back toUTC. The same value is on the phone number object astimezone. Atimezoneyou 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:00or ends after21:00is saved, and the response lists anoutside_calling_hoursentry inwarnings. 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_endearlier thandaily_window_startcrosses midnight:22:00–02:00dials from 10pm until 2am the next day. It is saved withwindow_crosses_midnightandoutside_calling_hourswarnings, 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 anempty_windowwarning and never dials. - Retry bounds.
max_attemptsis at most 10 andretry_backoff_minutesat least 30. Values outside them return400on that field. Staff can raise both for your organization; contact support.
{
"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:
curl -X POST https://api.thunderphone.com/v1/campaigns/3f6b2c9e-…/contacts \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-F 'file=@contacts.csv'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
phoneorphone_numbercolumn. Every other column becomes a per-contact variable. - JSON: a top-level list, or
{"contacts": [...]}— each object needsphone_number(orphone); 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
reasonfield names the rule. A contact that reaches dial time with such a number fails withlast_outcomedestination_not_allowedand is not retried.
{
"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:
| Action | Body | Effect |
|---|---|---|
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 https://api.thunderphone.com/v1/campaigns/3f6b2c9e-…/stats \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"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.