# Campaigns

> Docs index: https://wahasend.com/llms.txt · Everything in one file: https://wahasend.com/docs.md · API base URL: https://app.wahasend.com/api/v1

A campaign sends a template with `{{variables}}` to a list, spaced 3–7 seconds apart per message, now or at a set time. Comes with **Notify** (feature `campaigns`). Statuses: `draft` ⇄ `scheduled` → `sending` ⇄ `paused` → `completed`; `scheduled` or `paused` → `cancelled`. `stats` counts `sent`, `failed`, `delivered` and `read` (from WhatsApp's receipts, each message once).

<a id="campaign-create"></a>

### Create a campaign

`POST /api/v1/bulk/campaigns`

Saves it as a `draft`, or `scheduled` when you give `scheduledAt`. Nothing is sent until you call send (or the time comes).

- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | 1–255 characters. |
| `template` | string | yes | 1–4096 characters. `{{name}}`-style placeholders are filled from each recipient's `variables` (any language in the keys). |
| `recipients` | array | yes | 1–10,000 recipients. |
| `recipients[].phone` | string | yes | International phone number, 8–20 characters (`+22212345678`). `@lid` ids are not accepted here. |
| `recipients[].variables` | object (string → string) | no | Values for the template. |
| `scheduledAt` | string (ISO 8601) | no | When to start, with any offset: `2026-10-05T08:00:00Z` or `2026-10-05T08:00:00+00:00` (Nouakchott is UTC). Checked every minute; it starts from your default number. |

**Example**

curl:

```bash
curl -X POST https://app.wahasend.com/api/v1/bulk/campaigns \
  -H "X-API-Key: $WAHASEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Ramadan hours",
  "template": "Hello {{name}}, from Monday we open 10:00–16:00.",
  "recipients": [
    {
      "phone": "+22212345678",
      "variables": {
        "name": "Aïcha"
      }
    },
    {
      "phone": "+22287654321",
      "variables": {
        "name": "Moussa"
      }
    }
  ]
}'
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "name": "Ramadan hours",
    "template": "Hello {{name}}, from Monday we open 10:00–16:00.",
    "recipients": [
      {
        "phone": "+22212345678",
        "variables": {
          "name": "Aïcha"
        }
      },
      {
        "phone": "+22287654321",
        "variables": {
          "name": "Moussa"
        }
      }
    ]
  }),
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.post(
    "https://app.wahasend.com/api/v1/bulk/campaigns",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
    json={
        "name": "Ramadan hours",
        "template": "Hello {{name}}, from Monday we open 10:00–16:00.",
        "recipients": [
            {
                "phone": "+22212345678",
                "variables": {
                    "name": "Aïcha",
                },
            },
            {
                "phone": "+22287654321",
                "variables": {
                    "name": "Moussa",
                },
            },
        ],
    },
)
print(r.json())
```

**Response 201**

```json
{
  "success": true,
  "campaign": {
    "id": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
    "name": "Ramadan hours",
    "status": "draft",
    "recipientCount": 2,
    "scheduledAt": null
  }
}
```

**Errors to expect**

- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.

<a id="campaign-list"></a>

### List campaigns

`GET /api/v1/bulk/campaigns`

Every campaign of the organization, oldest first.

- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes

**Example**

curl:

```bash
curl https://app.wahasend.com/api/v1/bulk/campaigns \
  -H "X-API-Key: $WAHASEND_API_KEY"
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns', {
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.get(
    "https://app.wahasend.com/api/v1/bulk/campaigns",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```

**Response 200**

```json
{
  "success": true,
  "campaigns": [
    {
      "id": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
      "name": "Ramadan hours",
      "template": "Hello {{name}}, from Monday we open 10:00–16:00.",
      "status": "completed",
      "stats": {
        "total": 2,
        "sent": 2,
        "delivered": 2,
        "failed": 0,
        "read": 1
      },
      "scheduledAt": null,
      "createdAt": "2026-09-30T09:12:44.120Z"
    }
  ]
}
```

<a id="campaign-status"></a>

### Campaign status

`GET /api/v1/bulk/campaigns/:id/status`

Where a campaign stands, with its recipients and counts.

- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes

> `delivered` counts a message the first time WhatsApp reports it delivered or read; `read` the first time it is read. Campaigns from before 2026-10-01 have no `read`.

**Path parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |

**Example**

curl:

```bash
curl https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/status \
  -H "X-API-Key: $WAHASEND_API_KEY"
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/status', {
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.get(
    "https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/status",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```

**Response 200**

```json
{
  "success": true,
  "id": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
  "name": "Ramadan hours",
  "template": "Hello {{name}}, from Monday we open 10:00–16:00.",
  "recipients": [
    {
      "phone": "+22212345678",
      "variables": {
        "name": "Aïcha"
      }
    }
  ],
  "status": "sending",
  "stats": {
    "total": 2,
    "sent": 1,
    "delivered": 1,
    "failed": 0,
    "read": 0
  },
  "scheduledAt": null,
  "startedAt": "2026-09-30T09:15:02.000Z",
  "completedAt": null,
  "createdAt": "2026-09-30T09:12:44.120Z"
}
```

**Errors to expect**

- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.

<a id="campaign-update"></a>

### Edit before it starts

`PUT /api/v1/bulk/campaigns/:id`

Changes the name, template, recipients or start time of a `draft` or `scheduled` campaign.

- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes

**Path parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | 1–255 characters. |
| `template` | string | no | 1–4096 characters. |
| `recipients` | array | no | Replaces the whole list (1–10,000). |
| `recipients[].phone` | string | yes | International phone number, 8–20 characters (`+22212345678`). `@lid` ids are not accepted here. |
| `recipients[].variables` | object (string → string) | no | Values for the template. |
| `scheduledAt` | string (ISO 8601) \| null | no | A new start time (any offset) makes it `scheduled`; `null` makes it a `draft` again. |

**Example**

curl:

```bash
curl -X PUT https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71 \
  -H "X-API-Key: $WAHASEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "template": "Hello {{name}}, from Monday we open 10:00–16:30.",
  "scheduledAt": "2026-10-05T08:00:00+00:00"
}'
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71', {
  method: 'PUT',
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "template": "Hello {{name}}, from Monday we open 10:00–16:30.",
    "scheduledAt": "2026-10-05T08:00:00+00:00"
  }),
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.put(
    "https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
    json={
        "template": "Hello {{name}}, from Monday we open 10:00–16:30.",
        "scheduledAt": "2026-10-05T08:00:00+00:00",
    },
)
print(r.json())
```

**Response 200**

```json
{
  "success": true,
  "campaign": {
    "id": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
    "name": "Ramadan hours",
    "template": "Hello {{name}}, from Monday we open 10:00–16:30.",
    "status": "scheduled",
    "stats": {
      "total": 2,
      "sent": 0,
      "delivered": 0,
      "failed": 0
    },
    "scheduledAt": "2026-10-05T08:00:00.000Z",
    "createdAt": "2026-09-30T09:12:44.120Z"
  }
}
```

**Errors to expect**

- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.

<a id="campaign-send"></a>

### Start sending

`POST /api/v1/bulk/campaigns/:id/send`

Starts a `draft` or `scheduled` campaign now. Messages are queued 3–7 seconds apart.

- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes

**Path parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `containerId` | uuid | no | Which of your WhatsApp numbers to use; it always wins. Default: your default number (else any connected one), or a number of the group when [several numbers](https://wahasend.com/docs/phone-numbers.md#several-numbers) is on. Ignored with a per-number key, which always uses its own number. |

**Example**

curl:

```bash
curl -X POST https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/send \
  -H "X-API-Key: $WAHASEND_API_KEY"
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/send', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.post(
    "https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/send",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```

**Response 200**

```json
{
  "success": true,
  "campaignId": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
  "status": "sending",
  "totalRecipients": 2,
  "estimatedDuration": "1 minutes"
}
```

**Errors to expect**

- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.

<a id="campaign-pause"></a>

### Pause

`POST /api/v1/bulk/campaigns/:id/pause`

Stops the messages not sent yet. Only a `sending` campaign can pause.

- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes

**Path parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |

**Example**

curl:

```bash
curl -X POST https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/pause \
  -H "X-API-Key: $WAHASEND_API_KEY"
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/pause', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.post(
    "https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/pause",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```

**Response 200**

```json
{
  "success": true,
  "status": "paused",
  "jobsRemoved": 118
}
```

**Errors to expect**

- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.

<a id="campaign-resume"></a>

### Resume

`POST /api/v1/bulk/campaigns/:id/resume`

Sends what a `paused` campaign had left, never from the start.

- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes

**Path parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `containerId` | uuid | no | Which of your WhatsApp numbers to use; it always wins. Default: your default number (else any connected one), or a number of the group when [several numbers](https://wahasend.com/docs/phone-numbers.md#several-numbers) is on. Ignored with a per-number key, which always uses its own number. |

**Example**

curl:

```bash
curl -X POST https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/resume \
  -H "X-API-Key: $WAHASEND_API_KEY"
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/resume', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.post(
    "https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/resume",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```

**Response 200**

```json
{
  "success": true,
  "status": "sending",
  "jobsRequeued": 118
}
```

**Errors to expect**

- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.

<a id="campaign-delete"></a>

### Delete

`DELETE /api/v1/bulk/campaigns/:id`

Deletes a campaign that is not `sending` (pause it first) and its message history.

- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes

**Path parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |

**Example**

curl:

```bash
curl -X DELETE https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71 \
  -H "X-API-Key: $WAHASEND_API_KEY"
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71', {
  method: 'DELETE',
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.delete(
    "https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```

**Response 200**

```json
{
  "success": true
}
```

**Errors to expect**

- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.

<a id="campaign-cancel"></a>

### Cancel

`POST /api/v1/bulk/campaigns/:id/cancel`

Cancels a `scheduled` or `paused` campaign for good: status `cancelled`, the messages not sent yet are dropped (marked `failed`, error "Campaign cancelled"). To stop a `sending` campaign, pause it first.

- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes

**Path parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |

**Example**

curl:

```bash
curl -X POST https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/cancel \
  -H "X-API-Key: $WAHASEND_API_KEY"
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/cancel', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.post(
    "https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/cancel",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```

**Response 200**

```json
{
  "success": true,
  "status": "cancelled",
  "jobsRemoved": 0,
  "messagesCancelled": 118
}
```

**Errors to expect**

- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
