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

# Campaigns API: manage audiences and messaging

> Add contacts to evergreen campaigns, schedule sends per contact, cancel queued messages, and inspect campaign message status via the BluBash API.

The Campaigns API lets you programmatically manage the audience and message delivery of **evergreen** (continuous) campaigns in your BluBash workspace. You can add contacts individually or in bulk with per-contact scheduling, cancel queued messages before they are sent, and inspect message status at any point in the delivery lifecycle.

<Warning>
  The Campaigns feature must be enabled on your workspace subscription to use the write endpoints. Read endpoints (list and get messages) may work even without the feature enabled. If you attempt a write operation without the feature, you will receive a `403 Forbidden` response.
</Warning>

## Base URL

```text theme={null}
https://api.blubash.io/api/v1/campaigns
```

## Authentication

All requests require your Workspace API Key in the `X-API-Key` header.

```http theme={null}
X-API-Key: your_workspace_api_key
Content-Type: application/json
```

To obtain your API key, go to **Settings → API Keys** in your workspace admin panel.

## Prerequisites

Evergreen campaigns must be created and configured in the BluBash platform before you can use this API. The API covers audience ingestion and message dispatch only.

<Steps>
  <Step title="Create an evergreen campaign in the platform">
    In the BluBash admin panel, create a new campaign with `kind = EVERGREEN`.
  </Step>

  <Step title="Configure channel, message, and rate limits">
    Set up the channel, message template, and rate limit rules for the campaign.
  </Step>

  <Step title="Copy the campaignId">
    Save the campaign and copy the `campaignId` shown in the platform. You will use this ID in every API call below.
  </Step>
</Steps>

***

## Add contacts to audience

```http theme={null}
POST /api/v1/campaigns/:campaignId/audience
```

Adds one or more contacts to a campaign and defines the send schedule per contact. Each entry in the `items` array represents one contact and its scheduling configuration.

<Tip>
  To add a single contact, send the `items` array with one element — the endpoint is the same for both single and batch operations.
</Tip>

### Request body

<ParamField body="default_schedule_timezone" type="string">
  An IANA timezone name (e.g. `America/New_York`) applied to all items whose `datetime` has no UTC offset and no `schedule.timezone` specified.
</ParamField>

<ParamField body="items" type="array" required>
  An array of contact + schedule objects. At least one item is required.

  <Expandable title="Item fields">
    <ParamField body="contact_id" type="string">
      The ID of an existing contact in your workspace. Required if `identifier` is not provided.
    </ParamField>

    <ParamField body="identifier" type="string">
      The contact's channel identifier (e.g. a WhatsApp phone number). Required if `contact_id` is not provided. Used to look up or create the contact.
    </ParamField>

    <ParamField body="name" type="string">
      Display name for the contact. Used when creating a new contact record.
    </ParamField>

    <ParamField body="email" type="string">
      Email address of the contact. Used for lookup and contact creation.
    </ParamField>

    <ParamField body="custom_fields" type="object">
      Custom field values as a key-value object. Keys must be prefixed with `cf_` (e.g. `{ "cf_plan": "pro" }`).
    </ParamField>

    <ParamField body="tags" type="string[]">
      Tags to apply to the contact. Tags are normalized automatically (trimmed, lowercased, deduplicated) and applied additively — existing tags are not removed.
    </ParamField>

    <ParamField body="schedule" type="object">
      Send schedule for this contact. Defaults to `immediate` if omitted.

      <Expandable title="Schedule fields">
        <ParamField body="type" type="string" required>
          `immediate` to send as soon as possible, or `absolute` to schedule for a specific date and time.
        </ParamField>

        <ParamField body="datetime" type="string">
          ISO 8601 datetime string. Required when `type` is `absolute`. If the value includes a UTC offset or `Z`, the `timezone` field is ignored. If there is no offset, you must supply `schedule.timezone` or `default_schedule_timezone`.
        </ParamField>

        <ParamField body="timezone" type="string">
          IANA timezone name for converting a `datetime` without a UTC offset to UTC.
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

### Contact resolution

When you provide `identifier` instead of `contact_id`, the platform resolves the contact in this order:

1. Looks up the contact by the channel identifier (e.g. normalized WhatsApp number).
2. If not found and `email` is provided, searches by email (case-insensitive).
   * If found without a channel identifier, links the `identifier` to the contact.
   * If found with a **different** channel identifier, returns a `400` error to prevent silent identity merging.
3. If still not found, creates a new contact using `name`, `email`, and `identifier`.

### Scheduling rules

| `datetime` format                                   | Behavior                                                                       |
| --------------------------------------------------- | ------------------------------------------------------------------------------ |
| With `Z` or numeric offset (e.g. `+03:00`, `-0500`) | Treated as an absolute instant; `timezone` is ignored                          |
| Without offset (e.g. `2026-03-11T09:00:00`)         | Requires `schedule.timezone` or `default_schedule_timezone` for UTC conversion |

<Warning>
  If `datetime` has no UTC offset and no timezone is provided (either in `schedule.timezone` or `default_schedule_timezone`), the request returns a `400` error.
</Warning>

### Example request

```json theme={null}
{
  "default_schedule_timezone": "America/New_York",
  "items": [
    {
      "contact_id": "<CONTACT_ID_1>",
      "schedule": {
        "type": "immediate"
      }
    },
    {
      "identifier": "5511999999999",
      "name": "Jane Smith",
      "email": "jane@example.com",
      "custom_fields": {
        "cf_plan": "pro",
        "cf_source": "api"
      },
      "tags": ["Hot Lead", "VIP Customer"],
      "schedule": {
        "type": "absolute",
        "datetime": "2026-03-10T14:30:00.000Z"
      }
    },
    {
      "contact_id": "<CONTACT_ID_3>",
      "schedule": {
        "type": "absolute",
        "datetime": "2026-03-11T09:00:00",
        "timezone": "America/New_York"
      }
    }
  ]
}
```

### Response fields

<ResponseField name="items" type="array" required>
  One entry per input item, in the same order as the request.

  <Expandable title="Item fields">
    <ResponseField name="messageId" type="string">
      ID of the campaign message created for this contact.
    </ResponseField>

    <ResponseField name="contactId" type="string">
      ID of the resolved or created contact.
    </ResponseField>

    <ResponseField name="scheduled_at" type="string | null">
      The scheduled send time formatted in the IANA timezone or offset that was provided. `null` for immediate sends.
    </ResponseField>

    <ResponseField name="scheduled_at_utc" type="string | null">
      The canonical UTC send time (always ends in `Z`). `null` for immediate sends.
    </ResponseField>

    <ResponseField name="timezone" type="string | null">
      The IANA timezone name (e.g. `America/New_York`) or numeric offset (e.g. `-05:00`) used for scheduling. `null` for immediate sends.
    </ResponseField>
  </Expandable>
</ResponseField>

### Example response

```json theme={null}
{
  "items": [
    {
      "messageId": "<MESSAGE_ID>",
      "contactId": "<CONTACT_ID_1>",
      "scheduled_at": null,
      "scheduled_at_utc": null,
      "timezone": null
    },
    {
      "messageId": "<MESSAGE_ID>",
      "contactId": "<CONTACT_ID_2>",
      "scheduled_at": "2026-03-10T14:30:00.000Z",
      "scheduled_at_utc": "2026-03-10T14:30:00.000Z",
      "timezone": null
    },
    {
      "messageId": "<MESSAGE_ID>",
      "contactId": "<CONTACT_ID_3>",
      "scheduled_at": "2026-03-11T09:00:00.000-05:00",
      "scheduled_at_utc": "2026-03-11T14:00:00.000Z",
      "timezone": "America/New_York"
    }
  ]
}
```

***

## Cancel a queued message

```http theme={null}
POST /api/v1/campaigns/:campaignId/messages/:messageId/cancel
```

Cancels the delivery of a specific campaign message that is still in the queue. Cancelling one message does not affect any other messages in the campaign.

<Info>
  Only messages with status `QUEUED` can be cancelled. Messages that are already `SENDING`, `SENT`, or `FAILED` cannot be cancelled.
</Info>

### Response

```json theme={null}
{
  "id": "<MESSAGE_ID>",
  "status": "CANCELLED"
}
```

***

## List campaign messages

```http theme={null}
GET /api/v1/campaigns/:campaignId/messages?limit=50&offset=0
```

Returns a paginated list of messages for the campaign, each with its current status.

### Query parameters

<ParamField query="limit" type="number" default="50">
  Number of results to return per page.
</ParamField>

<ParamField query="offset" type="number" default="0">
  Number of results to skip for pagination.
</ParamField>

***

## Get a specific message

```http theme={null}
GET /api/v1/campaigns/:campaignId/messages/:messageId
```

Returns the details and current status of a single campaign message.

***

## Message statuses

| Status      | Description                            |
| ----------- | -------------------------------------- |
| `PENDING`   | Awaiting processing                    |
| `QUEUED`    | Queued for delivery — can be cancelled |
| `SENDING`   | Currently being sent                   |
| `SENT`      | Delivered successfully                 |
| `FAILED`    | Delivery failed                        |
| `CANCELLED` | Cancelled via API                      |

***

## Recommended workflow

<Steps>
  <Step title="Configure the campaign in the platform">
    Create an evergreen campaign in the BluBash admin panel and copy the `campaignId`.
  </Step>

  <Step title="Add contacts to the audience">
    Call `POST /api/v1/campaigns/:campaignId/audience` with your `items` array.
  </Step>

  <Step title="Choose a schedule per contact">
    Use `schedule.type = immediate` for instant delivery, or `schedule.type = absolute` with a datetime for scheduled delivery.
  </Step>

  <Step title="Cancel if needed">
    If you need to stop a queued message, call `POST /api/v1/campaigns/:campaignId/messages/:messageId/cancel`.
  </Step>

  <Step title="Monitor message status">
    Use `GET /api/v1/campaigns/:campaignId/messages` to inspect delivery status across the campaign.
  </Step>
</Steps>

***

## Code examples

<CodeGroup>
  ```bash Immediate send (cURL) theme={null}
  curl -X POST https://api.blubash.io/api/v1/campaigns/{campaignId}/audience \
    -H "X-API-Key: your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "items": [
        {
          "identifier": "5511999999999",
          "name": "Jane Smith",
          "tags": ["Hot Lead"],
          "schedule": {
            "type": "immediate"
          }
        }
      ]
    }'
  ```

  ```bash Scheduled send (cURL) theme={null}
  curl -X POST https://api.blubash.io/api/v1/campaigns/{campaignId}/audience \
    -H "X-API-Key: your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "default_schedule_timezone": "America/New_York",
      "items": [
        {
          "contact_id": "contact_abc123",
          "schedule": {
            "type": "absolute",
            "datetime": "2026-04-01T10:00:00"
          }
        }
      ]
    }'
  ```

  ```bash Batch with mixed schedules (cURL) theme={null}
  curl -X POST https://api.blubash.io/api/v1/campaigns/{campaignId}/audience \
    -H "X-API-Key: your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "default_schedule_timezone": "America/New_York",
      "items": [
        {
          "contact_id": "contact_001",
          "schedule": { "type": "immediate" }
        },
        {
          "identifier": "5511888887777",
          "name": "Pedro Lima",
          "custom_fields": {
            "cf_plan": "enterprise"
          },
          "tags": ["VIP Customer", "Hot Lead"],
          "schedule": {
            "type": "absolute",
            "datetime": "2026-04-05T09:00:00",
            "timezone": "America/New_York"
          }
        },
        {
          "contact_id": "contact_003",
          "schedule": {
            "type": "absolute",
            "datetime": "2026-04-06T12:00:00.000Z"
          }
        }
      ]
    }'
  ```

  ```bash Cancel a message (cURL) theme={null}
  curl -X POST https://api.blubash.io/api/v1/campaigns/{campaignId}/messages/{messageId}/cancel \
    -H "X-API-Key: your_api_key"
  ```

  ```bash List messages (cURL) theme={null}
  curl -X GET "https://api.blubash.io/api/v1/campaigns/{campaignId}/messages?limit=50&offset=0" \
    -H "X-API-Key: your_api_key"
  ```
</CodeGroup>

***

## Error reference

| Status | Message                                                                               | Cause                                                                   |
| ------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `400`  | `Each item must provide contact_id or identifier`                                     | An item has neither `contact_id` nor `identifier`                       |
| `400`  | `Identifier is invalid`                                                               | `identifier` is empty or invalid after normalization                    |
| `400`  | `schedule.timezone (IANA) is required when datetime has no UTC offset`                | `datetime` has no offset and no timezone was provided                   |
| `400`  | `Campaign is not an evergreen campaign`                                               | The `campaignId` belongs to a non-evergreen campaign                    |
| `400`  | `Cannot add contacts to cancelled or failed campaigns`                                | Campaign status is `CANCELLED` or `FAILED`                              |
| `400`  | `Cannot add contacts while the campaign is paused. Resume the campaign first.`        | Campaign status is `PAUSED`                                             |
| `400`  | `Contact found by email already has a different identifier for this campaign channel` | Email lookup found a contact with a conflicting channel identifier      |
| `400`  | `Contact does not have a valid channel identifier for this campaign channel`          | The resolved contact has no valid identifier for the campaign's channel |
| `401`  | —                                                                                     | Missing or invalid API key                                              |
| `403`  | `Campaigns feature is not available in your current plan`                             | The Campaigns feature is not enabled on your subscription               |
| `404`  | `Campaign not found`                                                                  | The `campaignId` does not exist in your workspace                       |
