# Campaigns

> Send bulk messages to multiple contacts

Source: https://docs.soosh.io/reference/api/campaigns/

## Overview

Campaigns allow you to send bulk WhatsApp messages to multiple contacts using approved templates. The API handles rate limiting and delivery tracking automatically.

## List Campaigns

Retrieve all campaigns.

`GET /api/campaigns`

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `limit` | integer | Page size, 1–100. Default `50`. |
| `status` | string | Filter by status |
| `whatsapp_account` | string | Filter by WhatsApp account **name** |
| `search` | string | Case-insensitive match on the campaign name |
| `from` / `to` | string | `YYYY-MM-DD` range filter on `created_at` (`to` is inclusive of the whole day) |

Ordered by `created_at DESC`.

### Response

```json
{
  "status": "success",
  "data": {
    "campaigns": [
      {
        "id": "uuid",
        "name": "Holiday Promotion",
        "whatsapp_account": "Main Business",
        "template_id": "uuid",
        "template_name": "holiday_offer",
        "status": "completed",
        "total_recipients": 1000,
        "sent_count": 1000,
        "delivered_count": 950,
        "read_count": 500,
        "failed_count": 50,
        "scheduled_at": "2024-01-01T10:00:00Z",
        "started_at": "2024-01-01T10:00:05Z",
        "completed_at": "2024-01-01T10:30:00Z",
        "created_by_name": "Jane Admin",
        "updated_by_name": "Jane Admin",
        "created_at": "2024-01-01T09:00:00Z",
        "updated_at": "2024-01-01T10:30:00Z"
      }
    ],
    "total": 25,
    "page": 1,
    "limit": 50,
    "summary": {
      "campaigns": 25,
      "sending": 1,
      "sent": 18400,
      "delivered": 17650,
      "read": 11200,
      "replied": 940,
      "failed": 310
    }
  }
}
```

`summary` adds up every campaign the filters match, not just this page: how many there are,
how many are sending now (`queued` or `processing`), and their message counts.

> **Note:** The account is referenced by **name** via `whatsapp_account` — there is no `account_id` field.
> Campaigns that use a media header also return `header_media_id`, `header_media_filename` and
> `header_media_mime_type`.

## Get Campaign

Retrieve a single campaign with detailed progress.

`GET /api/campaigns/{id}`

### Response

```json
{
  "status": "success",
  "data": {
    "id": "uuid",
    "name": "Holiday Promotion",
    "template_id": "uuid",
    "template_name": "holiday_offer",
    "whatsapp_account": "Main Business",
    "status": "processing",
    "total_recipients": 1000,
    "sent_count": 450,
    "delivered_count": 400,
    "read_count": 100,
    "failed_count": 10,
    "scheduled_at": null,
    "started_at": "2024-01-01T10:00:05Z",
    "created_at": "2024-01-01T09:00:00Z",
    "updated_at": "2024-01-01T10:05:00Z"
  }
}
```

## Create Campaign

Create a new campaign. Per-recipient template values are supplied when you import recipients, not
on the campaign itself — there is no `variable_mapping` field.

`POST /api/campaigns`

### Request Body

```json
{
  "name": "New Year Sale",
  "whatsapp_account": "Main Business",
  "template_id": "uuid",
  "header_media_id": "",
  "scheduled_at": "2024-01-01T00:00:00Z"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Campaign name |
| `whatsapp_account` | string | Yes | **Name** of the sending WhatsApp account |
| `template_id` | uuid | Yes | Template to send |
| `header_media_id` | string | No | Meta media ID for a media-header template — upload it first with `POST /api/campaigns/{id}/media` |
| `scheduled_at` | string | No | ISO-8601 time to start the campaign |

### Response

```json
{
  "status": "success",
  "data": {
    "id": "uuid",
    "name": "New Year Sale",
    "status": "draft",
    "created_at": "2024-01-01T00:00:00Z"
  }
}
```

## Update Campaign

Update a draft campaign.

`PUT /api/campaigns/{id}`

> **Note:** Only draft campaigns can be updated. Started or completed campaigns cannot be modified.

## Delete Campaign

Delete a campaign.

`DELETE /api/campaigns/{id}`

## Import Recipients

Import recipients from a CSV or JSON file.

`POST /api/campaigns/{id}/recipients/import`

### Request Body (JSON)

```json
{
  "recipients": [
    {
      "phone_number": "+1234567890",
      "recipient_name": "John Doe",
      "template_params": { "customer_name": "John", "coupon": "SAVE20" },
      "header_params": { "season": "Summer" }
    },
    {
      "phone_number": "+0987654321",
      "recipient_name": "Jane Smith",
      "template_params": { "customer_name": "Jane", "coupon": "SAVE15" },
      "header_params": { "season": "Winter" }
    }
  ]
}
```

`header_params` is only needed for templates with a TEXT header variable
(Meta allows at most one). It's kept separate from `template_params` so a
positional header `{{1}}` doesn't collide with body `{{1}}`. Omit the field
entirely for templates without a header variable.

### Response

```json
{
  "status": "success",
  "data": {
    "message": "Recipients added successfully",
    "added_count": 2,
    "total_recipients": 1002
  }
}
```

`added_count` is the number of recipients in this request; `total_recipients` is the campaign's new
total.

## Get Recipients

Get all recipients of a campaign with their per-recipient delivery status. This
endpoint returns the **full** recipient list for the campaign — it is not
paginated.

`GET /api/campaigns/{id}/recipients`

### Response

The `recipients` array holds every recipient; `total` is the count.

```json
{
  "status": "success",
  "data": {
    "recipients": [
      {
        "id": "uuid",
        "campaign_id": "uuid",
        "phone_number": "1234567890",
        "recipient_name": "John Doe",
        "status": "delivered",
        "template_params": { "customer_name": "John", "coupon": "SAVE20" },
        "header_params": { "season": "Summer" },
        "whatsapp_message_id": "wamid.xxx",
        "error_message": "",
        "sent_at": "2024-01-01T10:00:10Z",
        "delivered_at": "2024-01-01T10:00:15Z",
        "read_at": "2024-01-01T10:05:00Z"
      }
    ],
    "total": 1000
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `phone_number` | string | Recipient phone number |
| `recipient_name` | string | Recipient display name |
| `status` | string | `pending`, `sent`, `delivered`, `read`, or `failed` |
| `template_params` | object | Body parameter values used for this recipient |
| `header_params` | object | TEXT-header parameter values (if any) |
| `whatsapp_message_id` | string | Meta message ID once sent |
| `error_message` | string | Failure reason when `status` is `failed` |
| `sent_at` / `delivered_at` / `read_at` | string | Delivery timestamps (null until reached) |

> **Note:** When phone-number masking is enabled for the organization, `phone_number` and
> any phone-like `recipient_name` values are masked in the response.

## Delete Recipient

Remove a single recipient from a campaign. Only allowed while the campaign is in
`draft` status.

`DELETE /api/campaigns/{id}/recipients/{recipientId}`

### Response

```json
{
  "status": "success",
  "data": {
    "message": "Recipient deleted successfully"
  }
}
```

## Upload Campaign Media

Upload the media asset for a campaign whose template has a media header (image,
video, or document). Allowed only for `draft` campaigns whose template has a
non-TEXT header. The file is uploaded to WhatsApp and stored locally for preview.

`POST /api/campaigns/{id}/media`

Send the request as `multipart/form-data` with a `file` field (max 16 MB).

```bash
curl -X POST "http://your-server:8080/api/campaigns/{id}/media" \
  -H "X-API-Key: soosh_your_api_key" \
  -F "file=@banner.jpg"
```

### Response

```json
{
  "status": "success",
  "data": {
    "media_id": "meta-media-id",
    "filename": "banner.jpg",
    "mime_type": "image/jpeg",
    "local_path": "campaigns/uuid.jpg",
    "message": "Media uploaded successfully"
  }
}
```

## Get Campaign Media

Serve the previously uploaded campaign media file (used for previews). Returns
the raw media bytes, not a JSON envelope.

`GET /api/campaigns/{id}/media`

## Campaign Actions

### Start Campaign

Begin sending messages.

`POST /api/campaigns/{id}/start`

### Pause Campaign

Pause a running campaign.

`POST /api/campaigns/{id}/pause`

### Cancel Campaign

Cancel a campaign (cannot be resumed).

`POST /api/campaigns/{id}/cancel`

### Retry Failed

Re-queue every recipient whose delivery `failed`. Allowed only on `completed`,
`paused`, or `failed` campaigns. Failed recipients are reset to `pending`, the
campaign moves back to `processing`, and the messages are re-enqueued.

`POST /api/campaigns/{id}/retry-failed`

#### Response

```json
{
  "status": "success",
  "data": {
    "message": "Retrying failed messages",
    "retry_count": 12,
    "status": "processing"
  }
}
```

### Get Progress

Fetch live progress for a campaign. This returns the same payload as
[Get Campaign](#get-campaign) (status plus sent/delivered/read/failed counts) and
is convenient for polling while a campaign is sending.

`GET /api/campaigns/{id}/progress`

## Campaign Status

| Status | Description |
|--------|-------------|
| `draft` | Campaign created, not yet started |
| `scheduled` | Campaign scheduled for future sending |
| `queued` | Recipients have been pushed onto the Redis job queue |
| `processing` | Workers are actively sending messages |
| `paused` | Campaign is paused. When the system paused it (such as at the monthly broadcast limit), `paused_reason` says why |
| `completed` | All messages have been processed |
| `cancelled` | Campaign was cancelled |
| `failed` | The campaign could not be processed |

## Monthly Broadcast Limit

A super administrator can give a workspace a monthly limit on campaign messages (the `broadcast_messages_per_month` [workspace limit](https://docs.soosh.io/reference/api/contacts#workspace-limits)). Every campaign message sent counts, and a retried one counts again; a send that fails doesn't count. Months are calendar months in UTC.

- When the month is used up, running campaigns pause with `paused_reason` set, and their unsent recipients stay `pending`. Resume them next month, or once the limit is raised.
- [Start](#start-campaign) and [Retry Failed](#retry-failed) answer `403` with the reason while the month is used up.

`GET /api/campaigns/usage`

```json
{
  "status": "success",
  "data": { "used": 8200, "limit": 10000, "period": "2026-10", "resets_at": "2026-11-01T00:00:00Z" }
}
```

`limit` is `null` when the workspace has no monthly limit; messages are counted either way.

## Throughput

Sends are dispatched through a Redis Streams job queue and consumed by worker processes. Throughput
is governed by how many workers you run (`-workers=N` on `server` or `worker`), not by a built-in
per-second throttle — Soosh does **not** shape traffic to your Meta messaging tier. If you
exceed your tier, Meta rejects the sends and the affected recipients are marked `failed`; use
[Retry Failed](#retry-failed) once your limits allow.

> **Tip:** Start with smaller campaigns to warm up your account and improve your messaging tier.
