# Running campaigns

**Availability: Preview**

A campaign calls many recipients with one agent, on a schedule and at a controlled pace. This guide walks through creating a campaign, adding recipients, starting, pausing, resuming and stopping it, and reading per-recipient results.

Use a campaign whenever you call more than a handful of people for the same purpose: it paces calls for you (`concurrency_limit` at a time), screens every recipient against your compliance rules, and gives you one object to follow instead of hundreds of calls.

## Before you begin

| You need | For |
|---|---|
| `campaigns:write`, `campaigns:read` | Creating, starting and reading campaigns |
| `contacts:write` | Recipients are contacts: you upsert them first |
| A ready agent | In test mode, the Sample agent `agent_1ZltiWzxrXmMprGD6q63` |
| `webhooks:write` (optional) | To receive `campaign.*` events |

## The campaign lifecycle

```text
draft ──start──▶ running ──pause──▶ paused ──resume──▶ running
                   │                  │
                   ├──stop──▶ stopped ◀──stop──┤
                   ├──▶ completed  (every recipient worked through)
                   └──▶ failed     (the run ended in failure)

start again from paused, stopped, completed or failed ──▶ a new run (run_number + 1)
```

| Status | Meaning |
|---|---|
| `draft` | Created, never started. Nothing has been dialled. |
| `running` | A run is dialling, up to `concurrency_limit` calls at a time. |
| `paused` | No new calls start. Calls already connected finish normally (and are charged). |
| `stopped` | Ended by a stop request. No further calls start. |
| `completed` | The run worked through every recipient. |
| `failed` | The run ended in failure before finishing. |
| `unknown` | The state could not be determined. Not a success. Open enum. |

Every campaign carries `capabilities`, which says what is allowed **right now**. Check it instead of hard-coding the table:

| Action | Allowed from | `capabilities` field |
|---|---|---|
| Start | `draft`, `paused`, `stopped`, `completed`, `failed` | `start` |
| Pause | `running` | `pause` |
| Resume | `paused` | `resume` |
| Stop | `draft`, `running`, `paused` | `stop` |
| Edit (PATCH) | every status except `running` | `update` |
| Add recipients | `draft` only | `add_recipients` |

A campaign can also be **paused automatically**, for example when its budget cap is reached, the workspace balance runs out, the telephony connection drops, too many consecutive calls fail, or a start was refused. The `campaign.paused` event's `data.reason` says why (`budget_cap_reached`, `insufficient_balance`, `spend_limit_reached`, `telephony_disconnected`, `failure_streak`, `start_refused`, `account_inactive`, `requested`, `unknown`). Fix the cause, then resume.

## Create and start a campaign

Three steps: upsert the recipients as contacts, create the campaign with their ids, start it.

**1. Upsert each recipient as a contact.** Send one request per person (this one adds the test number that completes a call):

```bash
curl -X POST https://voice-api.relationsai.com/v1/contacts/upsert \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "phone_number": "+12025550100",
  "name": "Asha Rao"
}'
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/contacts/upsert", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "phone_number": "+12025550100",
    "name": "Asha Rao"
  }),
});
const contact = await response.json();
```
```python

response = requests.post(
    "https://voice-api.relationsai.com/v1/contacts/upsert",
    headers={
        "Authorization": "Bearer <token>",
    },
    json={
        "phone_number": "+12025550100",
        "name": "Asha Rao",
    },
    timeout=30,
)
contact = response.json()
```

Keep the `id` of each contact (`ct_8Rw2Tq5Lm9Xc3Vb7Nz4Kp1` here). Add `"consent_granted": true` only when the person really agreed to be called.

**2. Create the campaign** with the contact ids. It starts as a `draft`; nothing is dialled:

```bash
curl -X POST https://voice-api.relationsai.com/v1/campaigns \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "name": "November renewals",
  "agent_id": "agent_1ZltiWzxrXmMprGD6q63",
  "concurrency_limit": 2,
  "contact_ids": [
    "ct_8Rw2Tq5Lm9Xc3Vb7Nz4Kp1",
    "ct_2Mh7Yd4Qs8Fv1Kc6Bx9Tn3"
  ]
}'
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/campaigns", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "November renewals",
    "agent_id": "agent_1ZltiWzxrXmMprGD6q63",
    "concurrency_limit": 2,
    "contact_ids": [
      "ct_8Rw2Tq5Lm9Xc3Vb7Nz4Kp1",
      "ct_2Mh7Yd4Qs8Fv1Kc6Bx9Tn3"
    ]
  }),
});
const campaign = await response.json();
```
```python

response = requests.post(
    "https://voice-api.relationsai.com/v1/campaigns",
    headers={
        "Authorization": "Bearer <token>",
    },
    json={
        "name": "November renewals",
        "agent_id": "agent_1ZltiWzxrXmMprGD6q63",
        "concurrency_limit": 2,
        "contact_ids": [
            "ct_8Rw2Tq5Lm9Xc3Vb7Nz4Kp1",
            "ct_2Mh7Yd4Qs8Fv1Kc6Bx9Tn3",
        ],
    },
    timeout=30,
)
campaign = response.json()
```

Response `201`:

```json
{
  "id": "cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1",
  "object": "campaign",
  "livemode": false,
  "name": "November renewals",
  "agent_id": "agent_1ZltiWzxrXmMprGD6q63",
  "status": "draft",
  "capabilities": {
    "start": true,
    "pause": false,
    "resume": false,
    "stop": true,
    "update": true,
    "add_recipients": true
  },
  "concurrency_limit": 5,
  "budget_cap": {
    "amount": 500000,
    "currency": "INR"
  },
  "recipient_count": 2,
  "progress": {
    "calls": 0,
    "connected": 0,
    "no_answer": 0,
    "failed": 0,
    "in_progress": 0
  },
  "spend": {
    "amount": 0,
    "currency": "INR"
  },
  "run_number": 0,
  "dnc_screening": "enforced",
  "created_at": "2026-10-21T08:00:00.000Z"
}
```

**3. Start it**, using the `id` from that response:

```bash
curl -X POST https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/start \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/start", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const campaign = await response.json();
```
```python

response = requests.post(
    "https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/start",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
campaign = response.json()
```

Response `200`:

```json
{
  "id": "cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1",
  "object": "campaign",
  "livemode": false,
  "name": "November renewals",
  "agent_id": "agent_1ZltiWzxrXmMprGD6q63",
  "status": "running",
  "capabilities": {
    "start": false,
    "pause": true,
    "resume": false,
    "stop": true,
    "update": false,
    "add_recipients": false
  },
  "concurrency_limit": 5,
  "budget_cap": {
    "amount": 500000,
    "currency": "INR"
  },
  "recipient_count": 2,
  "progress": {
    "calls": 0,
    "connected": 0,
    "no_answer": 0,
    "failed": 0,
    "in_progress": 0
  },
  "spend": {
    "amount": 0,
    "currency": "INR"
  },
  "run_number": 1,
  "dnc_screening": "enforced",
  "created_at": "2026-10-21T08:00:00.000Z"
}
```

If a job stops halfway, check what already exists before sending a step again: every request is executed, so resending a create makes a second campaign. The complete example below looks for its campaign by name first, creates it only if it is missing, and starts it only while it is still a `draft`. An optional `Idempotency-Key` per step, saved with your batch, makes a re-run replay the steps that already happened instead (see [Idempotency](/fundamentals/idempotency)).

Complete runnable example: [cURL script](/examples/guides/running-campaigns/create-and-start.sh) · [JavaScript](/examples/guides/running-campaigns/create-and-start.mjs) · [Python](/examples/guides/running-campaigns/create-and-start.py)

The calls in order:

1. [`POST /contacts/upsert`](/api/contacts#create-or-update-a-contact-by-phone-number) for each person: recipients are always existing contacts (see [Managing contacts](/guides/managing-contacts)).
2. [`POST /campaigns`](/api/campaigns#create-a-campaign) with `name`, `agent_id`, optional `concurrency_limit` (default 1; lowered at start to your plan's `limits.campaign_max_concurrency`), optional `budget_cap` and up to 1000 `contact_ids`. The answer is `201` with the campaign in `draft`. **Nothing is dialled yet.**
3. Optionally [`POST /campaigns/{campaign_id}/recipients`](/api/campaigns#add-recipients) to add more contacts, up to 1000 per request and 10,000 per campaign, **only while the campaign is a draft**. Contacts already in the campaign are skipped, and the response says how many were `added`.
4. [`POST /campaigns/{campaign_id}/start`](/api/campaigns#start-a-campaign).

### What the start does

Before anything is dialled, the start checks the agent, the account, balance and spend limits, the campaign budget cap, calling hours and the telephony connection, then **screens every recipient** against the do-not-call list, consent and the per-number daily limit. Blocked recipients are left out of the run; if that leaves nobody, the start is refused with [`campaign_recipients_suppressed`](/errors/campaign_recipients_suppressed). After the start, `recipient_count` is the number of recipients this run calls.

- `200`: The run started; the campaign is `running`. Follow it.
- `202`: The start was sent but its confirmation has not arrived; the campaign shows `running` and is reconciled automatically. Read it again in a minute. **Do not start it again.**
- `409` [`campaign_outcome_unknown`](/errors/campaign_outcome_unknown): the start was sent and not confirmed; dialling may have begun. Wait `Retry-After`, then read the campaign: `running` means it started. Only if it did not, start it again (if you sent an idempotency key, use a new one: the same key returns this answer again).
- `409` [`campaign_dnc_override_enabled`](/errors/campaign_dnc_override_enabled): do-not-call screening was switched off for this campaign in the console (`dnc_screening: "disabled_in_console"`). The API never starts or resumes such a campaign. An admin switches the setting back on, or the campaign is started from the console.

In live mode a start counts **every recipient** against the workspace's daily limit of calls placed through the API, before dialling, because the run may call each of them. A start that would go past the limit is refused whole with [`daily_call_limit_reached`](/errors/daily_call_limit_reached) (`details.requested` and `details.remaining` say by how much), so a campaign larger than the daily limit cannot be started through the API. A resume never counts. Test mode has no daily limit.

**When the do-not-call list is checked.** Recipients are screened when a run starts and again when it resumes, not before each call. A number added to the do-not-call list while the campaign is running is still called by that run if it has not been reached yet. To apply the new entry, pause and resume the campaign (the resume is then refused while that number is a remaining recipient, see below) or stop it. `dnc_screening` on the campaign is `enforced` for every campaign the API can start.

## Follow results

Subscribe an endpoint to the `campaign.*` events (see [Receiving webhooks](/guides/receiving-webhooks)) and read the campaign and its recipients when they arrive:

| Event | When |
|---|---|
| `campaign.started` | A run started dialling (also when a `202` start is confirmed) |
| `campaign.paused` | Paused by a request or automatically; `data.reason` says why |
| `campaign.resumed` | A paused run resumed |
| `campaign.stopped` | Stopped; `data.reason` is `requested` |
| `campaign.completed` | The run worked through every recipient. In live mode up to about 15 minutes after the last call ends. |
| `campaign.failed` | The run ended in failure. In live mode up to about 15 minutes after it ends. |

Each call a campaign places is an ordinary call with `source: "campaign"` and `campaign_id` set, so `call.ended` and `call.settled` arrive for each one, and [`GET /calls`](/api/calls#list-calls) with `campaign_id` lists them.

Read the campaign:

```bash
curl -X GET https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1 \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const campaign = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
campaign = response.json()
```

And its recipients, 100 at a time (follow `next_cursor` as described in [Pagination](/fundamentals/pagination)):

```bash
curl -X GET "https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/recipients?limit=100" \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/recipients?limit=100", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const page = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/recipients",
    headers={
        "Authorization": "Bearer <token>",
    },
    params={
        "limit": 100,
    },
    timeout=30,
)
page = response.json()
```

Complete runnable example: [cURL script](/examples/guides/running-campaigns/follow-results.sh) · [JavaScript](/examples/guides/running-campaigns/follow-results.mjs) · [Python](/examples/guides/running-campaigns/follow-results.py)

A running campaign (`200`, with an `ETag` header):

```json
{
  "id": "cmp_7Wq3Er9Ty2Ui5Op8As4Df6",
  "object": "campaign",
  "livemode": true,
  "name": "November renewals",
  "agent_id": "agent_3hT9kQ2mVx7RbN4pLc8Wz1",
  "status": "running",
  "capabilities": { "start": false, "pause": true, "resume": false, "stop": true, "update": false, "add_recipients": false },
  "concurrency_limit": 5,
  "budget_cap": { "amount": 500000, "currency": "INR" },
  "recipient_count": 1200,
  "progress": { "calls": 640, "connected": 410, "no_answer": 190, "failed": 35, "in_progress": 5 },
  "spend": { "amount": 246000, "currency": "INR" },
  "run_number": 1,
  "dnc_screening": "enforced",
  "created_at": "2026-10-20T08:00:00.000Z"
}
```

`progress` counts the calls of the current or latest run (`calls`, `connected`, `no_answer`, `failed`, `in_progress`) and can lag by up to a minute. `spend` is what the run has cost so far (always zero in test mode).

[`GET /campaigns/{campaign_id}/recipients`](/api/campaigns#list-recipients) lists recipients in a stable order that does not change between pages (not by time), with what happened to each in the current or latest run:

| `status` | Meaning |
|---|---|
| `pending` | Not called yet in this run |
| `in_progress` | Being called |
| `completed` | Called; see `outcome` (the call `outcome` values) and `last_call_id` |
| `failed` | Every attempt failed |
| `suppressed` | Left out by screening; `suppression_reason` is `dnc_listed`, `consent_required`, `contact_daily_limit_reached` or `unknown` |
| `cancelled` | The run stopped before reaching them |
| `unknown` | Not known. Open enum. |

A page of recipients from a test-mode campaign (`200`):

```json
{
  "object": "list",
  "data": [
    {
      "id": "rcp_3Gh8Jk2Lz5Xc9Vb4Nm7Qw1",
      "object": "campaign_recipient",
      "livemode": false,
      "campaign_id": "cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1",
      "contact_id": "ct_8Rw2Tq5Lm9Xc3Vb7Nz4Kp1",
      "phone_number": "+12025550100",
      "status": "completed",
      "attempts": 1,
      "last_call_id": "call_2Hd8Ks4Lq7Mw3Nx9Pz5Rc1",
      "outcome": "completed",
      "suppression_reason": null
    },
    {
      "id": "rcp_6Tr1Yu4Io7Pa2Sd5Fg8Hj3",
      "object": "campaign_recipient",
      "livemode": false,
      "campaign_id": "cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1",
      "contact_id": "ct_2Mh7Yd4Qs8Fv1Kc6Bx9Tn3",
      "phone_number": "+12025550107",
      "status": "suppressed",
      "attempts": 0,
      "last_call_id": null,
      "outcome": null,
      "suppression_reason": "dnc_listed"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

Per-recipient results can be up to a minute behind the calls. One honest limit today: **in live mode a recipient left out by screening at start is not yet recorded per recipient**. It shows `pending` while the run is open and `unknown` once the run has ended (test mode records it as `suppressed`). The difference between the campaign's recipients and `recipient_count` after the start tells you how many were left out.

## Start, pause, resume and stop

Read the campaign first and check `capabilities`. Then send the action. Pause:

```bash
curl -X POST https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/pause \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/pause", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const campaign = await response.json();
```
```python

response = requests.post(
    "https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/pause",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
campaign = response.json()
```

Resume:

```bash
curl -X POST https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/resume \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/resume", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const campaign = await response.json();
```
```python

response = requests.post(
    "https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/resume",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
campaign = response.json()
```

Stop:

```bash
curl -X POST https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/stop \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/stop", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const campaign = await response.json();
```
```python

response = requests.post(
    "https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1/stop",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
campaign = response.json()
```

Each answers `200` with the campaign in its new status. After a timeout or a `retry: "yes"` error, read the campaign before sending the action again; after a `retry: "after"` error, wait `Retry-After` and send it again. On `campaign_outcome_unknown`, do not resend: wait `Retry-After` and read the campaign.

Complete runnable example: [cURL script](/examples/guides/running-campaigns/campaign-action.sh) · [JavaScript](/examples/guides/running-campaigns/campaign-action.mjs) · [Python](/examples/guides/running-campaigns/campaign-action.py)

- **Pause** ([`POST /campaigns/{campaign_id}/pause`](/api/campaigns#pause-a-campaign), from `running`): no new calls start. Calls already connected finish normally and are charged. The campaign is reported `paused` only once the pause is confirmed; if it is not, you get [`campaign_pause_not_confirmed`](/errors/campaign_pause_not_confirmed) and the campaign may still be dialling. Calls placed meanwhile are charged as usual. Read the campaign; if it is still running, send the pause again or stop it.
- **Resume** ([`POST /campaigns/{campaign_id}/resume`](/api/campaigns#resume-a-campaign), from `paused`): continues the same run with the settings it began with. Before anything is sent it repeats the account, balance, budget, calling-hours and telephony checks and **screens every recipient the run could still call again**. A resume cannot drop recipients from a run, so if any remaining recipient is now blocked (for example added to the do-not-call list after the start), nothing is resumed, the campaign stays `paused`, and you get [`campaign_recipients_blocked`](/errors/campaign_recipients_blocked) with `details.blocked_total`, `details.by_reason` and up to 100 `details.contact_ids`. **Do not stop and start it again**: a new start calls every recipient of the previous run again, including people already reached, and charges for those calls. To continue without the blocked people: list the recipients with `status=pending` (not reached yet), create a new campaign with those contacts minus the ones in `details.contact_ids`, start it, then stop this campaign (stopping does not hang up calls in progress). Never work around the refusal by removing do-not-call entries or recording consent nobody gave.
- **Stop** ([`POST /campaigns/{campaign_id}/stop`](/api/campaigns#stop-a-campaign), from `draft`, `running` or `paused`): no further calls start. **Calls already connected are not hung up**; they finish normally. Recipients the run had not reached become `cancelled`.
- **Start again** from `paused`, `stopped`, `completed` or `failed` begins a **new run** that calls the previous run's recipients again, **including people already reached** (and charges for those calls): from `paused` it abandons the paused run (use resume to continue it instead). `run_number` increases. Use it when you really want to call everyone again; to reach only the people not called yet, create a new campaign with them.

Every action is a POST. After a timeout read the campaign before resending (its `status` shows whether the action took effect); after a `retry: "after"` error wait and resend; stop on `retry: "no"`; and on `campaign_outcome_unknown` wait and read the campaign instead of resending.

## Edit safely

[`PATCH /campaigns/{campaign_id}`](/api/campaigns#update-a-campaign) changes `name`, `agent_id`, `concurrency_limit` or `budget_cap` (`null` removes the cap). Edits are allowed in every status except `running`, and **take effect at the next start**: a resume continues with the settings its run began with.

Campaigns use optimistic concurrency. Every read returns an `ETag` header; send it back in `If-Match`. If the campaign changed since you read it (someone edited it in the console, or it was paused automatically), you get [`version_conflict`](/errors/version_conflict) (412): read it again, decide again, and send the new ETag. A PATCH without `If-Match` is [`precondition_required`](/errors/precondition_required) (428). See [Concurrency](/fundamentals/concurrency).

Read the campaign and keep its `ETag` header, then send the change with it in `If-Match`:

```bash
curl -X PATCH https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1 \
     -H "Authorization: Bearer <token>" \
     -H 'If-Match: <etag>' \
     -H "Content-Type: application/json" \
     -d '{
  "name": "November renewals (second wave)",
  "concurrency_limit": 3
}'
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1", {
  method: "PATCH",
  headers: {
    "Authorization": "Bearer <token>",
    "If-Match": "<etag>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "November renewals (second wave)",
    "concurrency_limit": 3
  }),
});
const campaign = await response.json();
```
```python

response = requests.patch(
    "https://voice-api.relationsai.com/v1/campaigns/cmp_2Kd7Fg4Hj9Lz3Xc8Vb5Nm1",
    headers={
        "Authorization": "Bearer <token>",
        "If-Match": "<etag>",
    },
    json={
        "name": "November renewals (second wave)",
        "concurrency_limit": 3,
    },
    timeout=30,
)
campaign = response.json()
```

Complete runnable example: [cURL script](/examples/guides/running-campaigns/edit-settings.sh) · [JavaScript](/examples/guides/running-campaigns/edit-settings.mjs) · [Python](/examples/guides/running-campaigns/edit-settings.py)

## Budget, pace and limits

- `budget_cap`, as `{"amount": <minor units>, "currency": "INR"}`, is the spend at which the campaign is paused (reason `budget_cap_reached`). It is not an exact ceiling: the check runs about every 15 minutes, so spend can exceed the cap by the calls placed in that time. Set it with that margin in mind; raise or remove it with PATCH and resume. A start or resume at the cap is refused with [`campaign_budget_reached`](/errors/campaign_budget_reached).
- `concurrency_limit` is how many calls run at once. A higher value is lowered at start to `limits.campaign_max_concurrency` on [`GET /workspace`](/api/workspace#retrieve-the-workspace).
- Starts and resumes use the `dial` rate-limit class (30 per minute per workspace); pause, stop and edits use `write`.
- Calling hours apply to the whole campaign: a start or resume outside the calling window is refused with [`outside_calling_hours`](/errors/outside_calling_hours) (the window and the timezone it is evaluated in are in `details`).

## What can go wrong

- [`campaign_state_conflict`](/errors/campaign_state_conflict) (409): The action is not allowed in the current status (`details.status`, `details.action`). Read the campaign and use `capabilities`.
- [`campaign_operation_in_progress`](/errors/campaign_operation_in_progress) (409): Another start or resume of this campaign is running. Wait `Retry-After`, then send it again.
- [`campaign_outcome_unknown`](/errors/campaign_outcome_unknown) (409): A start or resume was sent but not confirmed. Wait, then read the campaign, and send the action again only if it did not start (with a new idempotency key if you use keys). If `details.needs_support` is true, contact support.
- [`campaign_dnc_override_enabled`](/errors/campaign_dnc_override_enabled) (409): Do-not-call screening is switched off for this campaign in the console. An admin switches it back on, or starts it from the console.
- [`campaign_has_no_recipients`](/errors/campaign_has_no_recipients) (409): Starting a campaign with no recipients. Add recipients while it is a draft.
- [`campaign_recipients_suppressed`](/errors/campaign_recipients_suppressed) (422): Every recipient is blocked by screening. Add callable recipients; review consent records.
- [`campaign_recipients_blocked`](/errors/campaign_recipients_blocked) (409): Resume found newly blocked recipients. Create a new campaign with the pending recipients minus the blocked ones, then stop this one.
- [`campaign_not_resumable`](/errors/campaign_not_resumable) (409): The paused run can no longer continue. A new start calls every previous recipient again; to reach only the people not called yet, create a new campaign with the pending recipients.
- [`campaign_recipients_unavailable`](/errors/campaign_recipients_unavailable) (503): Recipients could not be read for screening; nothing was dialled. Wait `Retry-After` and send it again. If it keeps failing on a very large campaign, create a new campaign with the pending recipients instead.
- [`campaign_too_large`](/errors/campaign_too_large) (422): Too many recipients for one run. Split the audience into several campaigns.
- [`recipient_contacts_not_found`](/errors/recipient_contacts_not_found) (422): Some `contact_ids` are not contacts of this workspace and mode. Upsert them first; `details` lists them.
- [`telephony_disconnected`](/errors/telephony_disconnected) (422): The telephony connection is not working; the campaign was paused. Reconnect it in the console, then start or resume.
- [`agent_not_ready`](/errors/agent_not_ready), [`insufficient_balance`](/errors/insufficient_balance), [`spend_limit_reached`](/errors/spend_limit_reached), [`account_inactive`](/errors/account_inactive): Same checks as a single call. See [Making an outbound call](/guides/making-an-outbound-call).

## Check it works in test mode

1. Run the create-and-start example. The campaign becomes `running` with `recipient_count: 3`.
2. Follow it: within about a minute (with `concurrency_limit: 2`) the recipients are `completed` with outcomes `completed`, `no_answer` and `completed`, and the campaign becomes `completed`.
3. Start another one with `concurrency_limit: 1` and pause it while at least one recipient is still `pending`. Add that recipient's number to the do-not-call list and resume: you get `409 campaign_recipients_blocked` listing it. Create a new campaign with the remaining pending recipients minus that one, start it, and stop the first.

## Next steps

[Managing contacts](/guides/managing-contacts).
