# Update a campaign. Only while it is a draft or scheduled.

Source: https://docs.usetone.ai/reference/campaigns/update

> Everything freezes once the campaign leaves draft: a running campaign is paused, edited and relaunched, not edited in place.

`PATCH /v1/campaigns/{id}`
Everything freezes once the campaign leaves draft: a running campaign is paused, edited and relaunched, not edited in place. `agentId` cannot be changed at any point.

Requires the `write` scope.

## Parameters

| Name | In | | Meaning |
|---|---|---|---|
| `id` | path | required | The campaign's id. |

## Request body

| Field | Type | | Meaning |
|---|---|---|---|
| `autoPauseBelowPaise` | integer | optional | Wallet floor in paise. When the balance falls below it the campaign pauses itself with pausedReason low_balance and emits campaign.paused, instead of dialling until calls start failing mid-conversation. Resume it after topping up. |
| `callsPerMinute` | integer | optional | How fast to dial, 1-60. The ceiling is the carrier's own campaign throttle, and bursts from a single caller ID are the pattern TCCCPR's spam rubric watches for — if you need more throughput, use more numbers rather than a higher rate. |
| `endsAt` | string | optional | Stop dialling at this instant even if recipients remain. Must be later than `startsAt`. |
| `maxConcurrent` | integer | optional | How many calls may be in flight at once, 1-20. 🔴 This does **not** queue above your organisation's concurrency quota — recipients dialled over it are refused and settle as `failed`. Check `GET /v1/limits` and keep this at or below what it reports. |
| `name` | string | optional | Your label for the campaign. Shown in the dashboard and on every call it places. |
| `numberId` | string | optional | The number to dial from. Must route to a Tone agent — a campaign on a BYO number is refused at launch rather than failing per call. |
| `retryAttempts` | integer | optional | How many times to redial a recipient whose outcome is in `retryOn`. 0-3; 0 disables retries. |
| `retryBackoff` | string | optional | How the wait grows between successive retries. Open set — handle an unrecognised value rather than throwing. Today: `linear`, `exponential`. |
| `retryIntervalMinutes` | integer | optional | Minutes to wait before the first retry, 5-1440. |
| `retryOn` | string[] | optional | Which outcomes earn a retry. 🔴 `answered` is deliberately not accepted — redialling someone who already spoke to the agent is what turns a campaign into a complaint, and under TCCCPR five complaints in ten days bars every number your organisation owns. |
| `startsAt` | string | optional | Do not dial before this instant. Omit to start as soon as the campaign is launched. |
| `weekdays` | integer | optional | Which weekdays may dial, as a 7-bit mask with **Monday = bit 0** (`0b0011111` = 31 = Mon-Fri, 127 = every day). `0` is refused: a campaign with no enabled weekday can never dial, and accepting it would leave it `running` and silently idle. |
| `windowEndMinute` | integer | optional | Latest minute of the day this campaign may dial, as minutes past midnight IST (1260 = 21:00). 1440 is accepted so a window can run to midnight. Must be later than `windowStartMinute`, and the **retry tail has to fit inside it too** — a schedule whose retries would land outside the window is refused at launch. |
| `windowStartMinute` | integer | optional | Earliest minute of the day this campaign may dial, as **minutes past midnight IST** (540 = 09:00). Narrower than the legal band, never wider: the compliance gate still applies and will block a call this window would have allowed. |

### Example request

```json
{
  "autoPauseBelowPaise": 50000,
  "callsPerMinute": 10,
  "endsAt": "2026-09-07T14:00:00Z",
  "maxConcurrent": 4,
  "name": "COD confirmations — September",
  "numberId": "9f1c2d84-4e3a-4f6c-b902-7d1e8a33c451",
  "retryAttempts": 1,
  "retryBackoff": "fixed",
  "retryIntervalMinutes": 60,
  "retryOn": [
    "no_answer",
    "busy"
  ],
  "startsAt": "2026-09-01T04:00:00Z",
  "weekdays": 31,
  "windowEndMinute": 1260,
  "windowStartMinute": 540
}
```

## Response

| Field | Type | | Meaning |
|---|---|---|---|
| `agent` | object | required | The agent as it was at launch. `id` goes null if the agent is later deleted; `name` keeps saying what ran. |
| `agent.id` | string | null | required | The agent's id, or null if it has since been deleted. |
| `agent.name` | string | null | required | The agent's name at launch. Kept even after the agent is deleted, so the record still says what ran. |
| `archivedAt` | string | null | required | When it was filed out of the list. Archived campaigns are hidden from `GET /v1/campaigns` unless `archived=true`; the detail route always serves them. |
| `autoPauseBelowPaise` | integer | required | Pause below this wallet balance. 0 means the floor is off. |
| `completedAt` | string | null | required | When the last recipient settled. RFC 3339, UTC. Null until the campaign finishes. |
| `createdAt` | string | required | RFC 3339, UTC. |
| `environment` | string | required | `live` or `test` — inherited from the credential that created it. |
| `id` | string | required | The campaign's unique id. |
| `launchedAt` | string | null | required | When the campaign was launched. RFC 3339, UTC. Null while it is still a draft. |
| `name` | string | required | Your label for the campaign. |
| `number` | object | required | The from-number as it was at launch. Same snapshot rule as `agent`. |
| `number.e164` | string | null | required | The number dialled from, in E.164. Kept after release, so the record still says which line called. |
| `number.id` | string | null | required | The number's id, or null if it has since been released. |
| `pacing` | object | required | How fast this campaign dials. |
| `pacing.callsPerMinute` | integer | required | How fast the campaign dials, in calls per minute. |
| `pacing.maxConcurrent` | integer | required | Above your concurrency quota this does not queue — the overflow settles as failed. See `GET /v1/limits`. |
| `pausedReason` | string | null | required | Why it stopped dialling. Null while healthy. Open set — handle an unrecognised value rather than throwing. Today: `operator`, `low_balance`, `target_unavailable`, `carrier_errors`. |
| `preflight` | object | null | required | The cached pre-flight funnel, or null if it has not been computed yet. Launch recomputes rather than trusting it. |
| `progress` | object | required | Counters cached on the campaign row, written alongside the recipient rows they summarise. If they ever disagree with the recipients, the recipients are right. |
| `progress.connected` | integer | required | Recipients who answered. Never exceeds `dialed`. |
| `progress.dialed` | integer | required | Recipients dialled at least once. Retries do not double-count. |
| `progress.excluded` | integer | required | Recipients the pre-dial gate removed — suppression list, missing consent, outside the calling window. Never dialled, never billed. |
| `progress.spentPaise` | integer | required | What this campaign has cost so far, in paise. |
| `progress.total` | integer | required | Recipients uploaded, before exclusions. |
| `purpose` | string | null | required | The sender classification this campaign ran under, snapshotted at launch. It decides which calling window applied, so it is evidence — not a live read of the agent. |
| `retries` | object | required | How this campaign retries recipients it did not reach. The retry tail has to fit inside the calling window, or the campaign is refused at launch. |
| `retries.attempts` | integer | required | How many times a recipient is redialled before giving up. |
| `retries.backoff` | string | required | Retry spacing. Open set — handle an unrecognised value rather than throwing. Today: `linear`, `exponential`. |
| `retries.intervalMinutes` | integer | required | Minutes before the first retry. |
| `retries.on` | string[] | required | Which dispositions are retried. Open set — handle an unrecognised value rather than throwing. Today: `answered`, `no_answer`, `busy`, `failed`, `voicemail`, `unknown`. |
| `schedule` | object | required | When this campaign is allowed to dial. Narrower than the legal window, never wider — the compliance gate still runs on every call. |
| `schedule.endsAt` | string | null | required | Stop dialling at this instant even if recipients remain. RFC 3339, UTC. |
| `schedule.startsAt` | string | null | required | Do not dial before this instant. RFC 3339, UTC. |
| `schedule.weekdays` | integer | required | 7-bit mask, Monday = bit 0. |
| `schedule.windowEndMinute` | integer | required | Minutes from midnight IST. |
| `schedule.windowStartMinute` | integer | required | Minutes from midnight IST. Dialling never happens outside the window. |
| `status` | string | required | Lifecycle position. `paused` is resumable; `stopped` is terminal and abandons the remaining recipients. Open set — handle an unrecognised value rather than throwing. Today: `draft`, `scheduled`, `running`, `paused`, `completed`, `stopped`, `failed`. |
| `updatedAt` | string | required | When the campaign was last changed. RFC 3339, UTC. |

### Example response

```json
{
  "data": {
    "agent": {
      "id": "9f1c2d84-4e3a-4f6c-b902-7d1e8a33c451",
      "name": "Order confirmation"
    },
    "archivedAt": null,
    "autoPauseBelowPaise": 50000,
    "completedAt": null,
    "createdAt": "2026-08-25T09:14:22Z",
    "environment": "live",
    "id": "9f1c2d84-4e3a-4f6c-b902-7d1e8a33c451",
    "launchedAt": "2026-09-01T04:00:00Z",
    "name": "COD confirmations — September",
    "number": {
      "e164": "+918045678901",
      "id": "4c8b1e77-3d2a-4f5b-9e6c-8a7b6c5d4e3f"
    },
    "pacing": {
      "callsPerMinute": 10,
      "maxConcurrent": 4
    },
    "pausedReason": "operator",
    "preflight": {
      "callable": 0,
      "checks": [
        {
          "outcome": "pass",
          "reason": "Outside the 09:00-21:00 IST window for promotional calls.",
          "type": "time_window"
        }
      ],
      "computedAt": "2026-09-01T03:58:12Z",
      "estimatedCostPaise": {
        "high": 480000,
        "low": 120000
      },
      "excluded": "invalid_number",
      "excludedE164s": {},
      "total": 0,
      "walletBalancePaise": 500000
    },
    "progress": {
      "connected": 244,
      "dialed": 320,
      "excluded": 31,
      "spentPaise": 146400,
      "total": 500
    },
    "purpose": "transactional",
    "retries": {
      "attempts": 1,
      "backoff": "linear",
      "intervalMinutes": 60,
      "on": [
        "no_answer",
        "busy"
      ]
    },
    "schedule": {
      "endsAt": "2026-09-07T14:00:00Z",
      "startsAt": "2026-09-01T04:00:00Z",
      "weekdays": 31,
      "windowEndMinute": 1260,
      "windowStartMinute": 540
    },
    "status": "running",
    "updatedAt": "2026-08-25T09:14:22Z"
  }
}
```
