# Campaign lifecycle

Source: https://docs.usetone.ai/flows/campaign-lifecycle

> Draft to finished, and every state it can be in on the way.

See [Campaigns](/campaigns) for the full field reference.

| # | Call | State after |
|---|---|---|
| 1 | `POST /v1/campaigns` | `draft` |
| 2 | `POST /{id}/recipients` — up to 500 per request | `draft` |
| 3 | `POST /{id}/preflight` | `draft` — nothing changes |
| 4 | `POST /{id}/launch` **with `Idempotency-Key`** | `running` |
| 5 | `POST /{id}/pause` / `resume` | `paused` / `running` |
| 6 | Last recipient settles, or `POST /{id}/stop` | `completed` |

## Pre-flight is free and tells you everything

It returns the exclusion buckets — `invalid_number`, `dnc`, `frequency_cap`,
`duplicate`, `dnd_scrub`, `blocked_by_gate` — with each recipient counted against
exactly one reason, plus the compliance checks, an estimated cost band and your
balance. It changes nothing.

A `block` verdict makes launch a `422`. A `warn` does not.

⚠️ `duplicate` always reads `0` here — duplicates are dropped when recipients are
added, so the real count is in the add-recipients response.

## Launch is the point of no return

🔴 `Idempotency-Key` is **required**; without it, `400`. Launching twice dials
the whole list twice, which is exactly the complaint pattern TCCCPR punishes.

Launch also refuses when:

- the agent is still a **draft**,
- the number routes to **your own stack** — refused at the button rather than as
  thousands of per-call `422`s,
- the calling window plus the **retry tail** would fall outside the legal band,
- **scrubbing fails**. A scrubber outage fails the launch with a `503` rather
  than passing an unscrubbed list. That is the one outcome TCCCPR forbids.

Launch snapshots the agent, the number and the purpose. The run finishes on the
version it launched with.

## While it runs

```bash
curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/campaigns/$C/outcomes"
curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/calls?campaign_id=$C"
```

The recipient list names each recipient's **last** attempt. For every call
including retries, filter the call log by `campaign_id`.

It can pause itself: below `autoPauseBelowPaise` it stops with
`pausedReason: "low_balance"` and emits `campaign.paused`. Top up, then resume.

## Changing a running campaign

You cannot. `agentId` can never change, and everything else freezes once the
campaign leaves draft. Pause, publish your prompt change, and relaunch — or
`POST /{id}/duplicate`, which copies the campaign and its recipients into a fresh
draft.

## After it finishes

`POST /{id}/recipients/{recipientId}/redial` queues one settled recipient for
another attempt, and a completed campaign wakes up for it. `POST /{id}/archive`
hides it from the list without deleting anything.
