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

# Campaigns

> Call a list of recipients automatically: Wave paces the calls, retries the ones that did not answer, and tells you each outcome.

A **campaign** calls a list of recipients for you. You give Wave the numbers, a
stored callflow and a caller ID. Wave calls each recipient, retries the ones that
did not answer, and runs machine detection on each answered call. A person hears
your callflow. An answering machine gets your voicemail message, if you set one.

> [!NOTE]
>
> Campaigns ship behind a per-environment flag. Until they are enabled on your
> environment, every endpoint returns `503 CAMPAIGNS_DISABLED`. A campaign runs a
> stored callflow, so create also needs [stored callflows](https://docs.wave.sa/voice/ivr.md) on your
> environment. Without them, create returns `409 CAMPAIGN_NEEDS_IVR`.

## Create a campaign

`POST /v1/campaigns` creates a campaign. Needs the `campaigns:write` scope. Send
an `Idempotency-Key` header: a retry with the same key returns the first campaign
and does not create a second one. Wave keeps the key for 24 hours. A refused
request is not kept.

```bash cURL theme={null}
curl -X POST https://api.wave.sa/v1/campaigns \
  -H "Authorization: Bearer $WAVE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: renewals-2026-11-02" \
  -d '{
    "name": "Policy renewals",
    "callflow_name": "renewal-menu",
    "caller_id": "+966590007001",
    "recipients": [
      { "phone": "+966500000001", "reference": "policy-1001" },
      { "phone": "+966500000002", "reference": "policy-1002" }
    ],
    "start_at": "2026-11-02T09:00:00+03:00",
    "calling_window": { "start": "09:00", "end": "17:00" },
    "max_attempts": 3,
    "retry_after_minutes": 30,
    "max_concurrent": 5,
    "voicemail_message": {
      "type": "say",
      "text": "Your policy is due for renewal. Please call us back.",
      "language": "en"
    }
  }'
```

```javascript Node.js theme={null}
const res = await fetch("https://api.wave.sa/v1/campaigns", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WAVE_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "renewals-2026-11-02",
  },
  body: JSON.stringify({
    name: "Policy renewals",
    callflow_name: "renewal-menu",
    caller_id: "+966590007001",
    recipients: [
      { phone: "+966500000001", reference: "policy-1001" },
      { phone: "+966500000002", reference: "policy-1002" },
    ],
    start_at: "2026-11-02T09:00:00+03:00",
    calling_window: { start: "09:00", end: "17:00" },
    max_attempts: 3,
    retry_after_minutes: 30,
    max_concurrent: 5,
    voicemail_message: {
      type: "say",
      text: "Your policy is due for renewal. Please call us back.",
      language: "en",
    },
  }),
});
console.log(await res.json());
```

The response is `201` with the campaign, and two counts: `recipients_accepted`
and `duplicates_removed`.

```json theme={null}
{
  "id": "5b1c…",
  "name": "Policy renewals",
  "status": "scheduled",
  "callflow_name": "renewal-menu",
  "caller_id": "+966590007001",
  "start_at": "2026-11-02T06:00:00.000Z",
  "calling_window": { "start": "09:00", "end": "17:00" },
  "max_attempts": 3,
  "retry_after_minutes": 30,
  "max_concurrent": 5,
  "voicemail_message": {
    "type": "say",
    "text": "Your policy is due for renewal. Please call us back.",
    "language": "en"
  },
  "sandbox": false,
  "created_at": "2026-10-30T10:00:00.000Z",
  "started_at": null,
  "completed_at": null,
  "recipients_accepted": 2,
  "duplicates_removed": 0
}
```

### Fields

| Field | Required | Rules |
| - | - | - |
| `name` | Yes | 1–100 characters. |
| `callflow_name` | Yes | 1–100 characters. The name of a stored callflow of your account. It must be a [menu or a multi-level menu](https://docs.wave.sa/voice/ivr.md). |
| `caller_id` | Production only | A KSA number that your account owns. A sandbox key ignores it. |
| `recipients` | Yes | 1–10,000 recipients (5 with a sandbox key). Each has a KSA `phone` and an optional `reference` (at most 64 characters). Wave never speaks the `reference`. |
| `start_at` | No | ISO 8601 time with an offset. With no `start_at`, or a time in the past, the campaign starts at once (`running`). A future time gives `scheduled`. |
| `calling_window` | No | `{ "start": "HH:MM", "end": "HH:MM" }`, KSA time. Wave calls only inside this daily window. The window can cross midnight; `start` and `end` must differ. With no window, Wave calls at any time of day. |
| `max_attempts` | No | 1–5. Default `3`. |
| `retry_after_minutes` | No | 10–1440. Default `30`. |
| `max_concurrent` | No | The most calls of this campaign at the same time. Default: half your concurrency cap (at least 1). At most your concurrency cap − 1 (at least 1), so one line stays free for your other calls. |
| `voicemail_message` | No | What an answering machine gets. `{ "type": "say", "text": "…", "language": "ar" }` (text at most 500 characters; `language` is `ar` or `en`), or `{ "type": "play", "url": "https://…" }` (a public HTTPS audio URL). |
| `environment` | No | `sandbox` or `production`. A key creates a campaign of its own environment. A value that is not the key's environment returns `400`. |

The request body can be at most 2 MB. Wave removes duplicate phone numbers: the
first row of a number stays.

### Sandbox and production

* **Sandbox key:** at most 5 recipients. The campaign always presents the shared
  sandbox number, and Wave ignores `caller_id`.
* **Production key:** `caller_id` is required. A number that your account does not
  own returns `403 CLI_NOT_OWNED`.

### Activate the callflow first

> [!WARNING]
>
> A person hears the **active** version of the callflow. Create accepts a
> callflow that has no active version. Then every answered call ends and the
> person hears nothing, but the outcome is still `answered`.
> [Activate a version](https://docs.wave.sa/voice/ivr.md) of the callflow before the campaign starts.

## How Wave calls

* **Pacing.** Wave starts new calls only while the campaign is below
  `max_concurrent`, your account is below its concurrency cap, and your
  [call limits](https://docs.wave.sa/voice/calls-and-recordings.md#call-velocity-limits) allow a new
  call. When your call limits stop a call, or when outbound calling is suspended
  on your account, the recipient waits for a later try. Wave does not count an
  attempt.
* **When.** Wave calls from `start_at`, and only inside the calling window (KSA
  time). During quiet hours, a call waits until the quiet hours end.
* **Opted-out numbers.** A recipient who opted out is not called. Its outcome is
  `skipped`.
* **On each answer.** Machine detection runs first:
  * A person (or Wave is not sure): the person hears your callflow.
  * An answering machine, with a voicemail message: Wave waits for the beep, plays
    or speaks the message, then hangs up. If no beep comes, Wave also plays the
    message and hangs up.
  * An answering machine, with no voicemail message: Wave hangs up.

## Outcomes and retries

Each recipient ends with one outcome:

| Outcome | Meaning |
| - | - |
| `answered` | A person answered (or Wave was not sure). |
| `voicemail` | An answering machine answered. |
| `no_answer` | Nobody answered, after the last attempt. |
| `busy` | The line was busy, after the last attempt. |
| `failed` | The call could not be placed or connected. |
| `skipped` | The recipient opted out. Wave did not call. |
| `cancelled` | The campaign was stopped before the recipient was done. |

**Retries:** after `no_answer`, `busy` or `failed`, Wave tries again after
`retry_after_minutes`, until the recipient has `max_attempts` attempts. Then that
outcome is final. Two cases are `failed` at once, with no retry:

* The voice engine sent no signal for the call 5 minutes after Wave sent it.
* Wave or the voice engine refused the call, or the voice engine cancelled it.

## Pause, resume and stop

A campaign has one of these statuses:

| Status | Meaning |
| - | - |
| `scheduled` | Waits for `start_at`. |
| `running` | Wave places calls. |
| `paused` | Wave places no new calls. |
| `stopped` | Stopped by you. This cannot be undone. |
| `completed` | Every recipient has an outcome. |

Use `POST /v1/campaigns/{id}/pause`, `/resume` and `/stop`. Each needs the
`campaigns:write` scope and returns the campaign.

| Action | From | To |
| - | - | - |
| `pause` | `running` | `paused` |
| `resume` | `paused` | `running` |
| `stop` | `scheduled`, `running` or `paused` | `stopped` |

Any other move returns `409 CAMPAIGN_INVALID_STATE`. On pause or stop, calls that
are in progress go on to their end. Stop gives `cancelled` to every recipient that is not
done yet.

```bash cURL theme={null}
curl -X POST https://api.wave.sa/v1/campaigns/5b1c…/pause \
  -H "Authorization: Bearer $WAVE_API_KEY"
```

```javascript Node.js theme={null}
const res = await fetch("https://api.wave.sa/v1/campaigns/5b1c…/pause", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WAVE_API_KEY}`,
  },
});
console.log(await res.json());
```

## Read a campaign and its recipients

All reads need the `campaigns:read` scope. A sandbox key sees only sandbox
campaigns, and a production key only production campaigns.

* `GET /v1/campaigns` lists the project's campaigns, newest first, as a cursor
  page (`limit` default 20, at most 100).
* `GET /v1/campaigns/{id}` returns the campaign with `counts` (`total`,
  `by_state`, `by_outcome`) and `estimated_cost_sar`. The cost is an estimate
  only: billing can be different.
* `GET /v1/campaigns/{id}/recipients` lists the recipients, newest first, as a
  cursor page (`limit` default 50, at most 100). Filter with `?outcome=`.

```bash cURL theme={null}
curl "https://api.wave.sa/v1/campaigns/5b1c…/recipients?outcome=no_answer&limit=50" \
  -H "Authorization: Bearer $WAVE_API_KEY"
```

```javascript Node.js theme={null}
const res = await fetch(
  "https://api.wave.sa/v1/campaigns/5b1c…/recipients?outcome=no_answer&limit=50",
  {
    headers: {
      Authorization: `Bearer ${process.env.WAVE_API_KEY}`,
    },
  }
);
console.log(await res.json());
```

```json theme={null}
{
  "data": [
    {
      "id": "c07e…",
      "phone": "+966500000002",
      "reference": "policy-1002",
      "state": "done",
      "outcome": "no_answer",
      "attempts": 3,
      "next_attempt_at": null,
      "last_call_id": "27d6dd39-…",
      "machine_detection_result": null,
      "updated_at": "2026-11-02T10:05:00.000Z"
    }
  ],
  "next_cursor": null,
  "limit": 50
}
```

Pass `next_cursor` back as `?cursor=` to get the next page. A recipient's `state`
is `pending`, `waiting`, `dialling` or `done`. `machine_detection_result` is
`human`, `machine`, `notsure`, or `null` when detection did not run.

## Webhooks

Subscribe to two [webhook](https://docs.wave.sa/webhooks.md) events instead of polling. They carry **no
phone number**: match on `reference` or `recipient_id`.

`campaign.recipient_completed` — a recipient got its final outcome:

```json theme={null}
{
  "event": "campaign.recipient_completed",
  "data": {
    "campaign_id": "5b1c…",
    "recipient_id": "c07e…",
    "reference": "policy-1002",
    "outcome": "voicemail",
    "attempts": 1,
    "last_call_id": "27d6dd39-…",
    "machine_detection_result": "machine"
  }
}
```

`campaign.completed` — every recipient has an outcome (`status: "completed"`), or
you stopped the campaign (`status: "stopped"`). `by_outcome` lists every
outcome, with `0` when there is none:

```json theme={null}
{
  "event": "campaign.completed",
  "data": {
    "campaign_id": "5b1c…",
    "status": "completed",
    "counts": {
      "total": 2,
      "by_outcome": {
        "answered": 1,
        "voicemail": 1,
        "no_answer": 0,
        "busy": 0,
        "failed": 0,
        "skipped": 0,
        "cancelled": 0
      }
    }
  }
}
```

## Errors

| Status | Code | When |
| - | - | - |
| `400` | `VALIDATION_ERROR` | A field breaks a rule above. The message names the field. |
| `403` | `CLI_NOT_OWNED` | `caller_id` is not a number that your account owns. |
| `403` | `INSUFFICIENT_PERMISSIONS` | The key does not have the `campaigns:read` or `campaigns:write` scope. |
| `404` | `CAMPAIGN_NOT_FOUND` | No such campaign in your project and environment. |
| `409` | `CAMPAIGN_INVALID_STATE` | The campaign cannot make that move from its status. |
| `409` | `CAMPAIGN_LIMIT_REACHED` | Your account can hold at most 50,000 recipients that are not done yet, across all its campaigns. Stopped and completed campaigns do not count. |
| `409` | `CAMPAIGN_NEEDS_IVR` | Stored callflows are not enabled on this environment. |
| `503` | `CAMPAIGNS_DISABLED` | Campaigns are not enabled on this environment. |

## Next steps

> **[IVR menus](https://docs.wave.sa/voice/ivr.md)**
>
> Build the callflow that a person hears.

> **[Webhooks](https://docs.wave.sa/webhooks.md)**
>
> Get each outcome pushed to you instead of polling.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.