> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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` →) `running` → `completed`, with
`paused` and `cancelled` reachable via [actions](#campaign-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

```json theme={null}
{
  "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"
}
```

| Field                                                                 | Type              | Description                                                                                          |
| --------------------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
| `public_id`                                                           | UUID              | Campaign id (used in every path)                                                                     |
| `name`                                                                | string            | 1–255 chars                                                                                          |
| `agent`                                                               | integer           | The [Agent](/api-reference/agents) that speaks on every call (writable)                              |
| `agent_name`                                                          | string            | Read-only convenience                                                                                |
| `from_number`                                                         | string            | An outbound-capable number in your org. **Demo numbers are rejected**                                |
| `status`                                                              | string            | `draft`, `scheduled`, `running`, `paused`, `completed`, `cancelled` (read-only — change via actions) |
| `timezone`                                                            | string            | IANA timezone the daily window is evaluated in. Default `UTC`                                        |
| `daily_window_start`, `daily_window_end`                              | time              | Local calling window, e.g. `"09:00:00"`–`"17:00:00"`                                                 |
| `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, default 2 — total tries per contact                                                             |
| `retry_backoff_minutes`                                               | integer           | Delay before a retryable contact is attempted again. Default 60                                      |
| `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)                                                                        |

***

## Create a campaign

<CodeGroup>
  ```bash cURL theme={null}
  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"]
    }'
  ```
</CodeGroup>

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](#contact-object); 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:

<CodeGroup>
  ```bash CSV upload theme={null}
  curl -X POST https://api.thunderphone.com/v1/campaigns/3f6b2c9e-…/contacts \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -F 'file=@contacts.csv'
  ```

  ```bash JSON theme={null}
  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" }
      ]
    }'
  ```
</CodeGroup>

* **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.

```json Response (201 Created) theme={null}
{
  "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](/api-reference/calls).

***

## 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 |
| `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

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/campaigns/3f6b2c9e-…/stats \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response theme={null}
{
  "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.

***

## Related

<CardGroup cols={2}>
  <Card title="Outbound Calls" icon="arrow-up-right" href="/api-reference/outbound-calls">
    Place a single ad-hoc outbound call instead.
  </Card>

  <Card title="Phone Numbers" icon="phone" href="/api-reference/phone-numbers">
    Campaigns need an outbound-capable (non-demo) number.
  </Card>

  <Card title="Agents" icon="robot" href="/api-reference/agents">
    The agent that speaks on campaign calls.
  </Card>

  <Card title="Calls" icon="phone-volume" href="/api-reference/calls">
    Every attempt appears in call history.
  </Card>
</CardGroup>
