# Create a campaign

> A campaign is a /v1 broadcast; the id works on /v1/broadcasts/{broadcastId}. Runs POST /v1/broadcasts with the sender resolved from from. A repeated idempotencyKey answers 409 conflict with the original in details.campaign. A channel the resolved sender does not carry (smart: no channel at all) is refused at creation with 409 no_channel, as on POST /v2/messages, rather than created and failed at send. from with a channel it does not carry (a sms campaign from: "sms_oneway") is that same 409 no_channel, in the words POST /v2/messages uses (details.connected, and sms vs sms_oneway told apart); channel only picks between senders that share an address, it never makes an existing address 404.

`POST https://api.zavu.dev/v2/campaigns`

A campaign is a /v1 broadcast; the id works on `/v1/broadcasts/{broadcastId}`. Runs `POST /v1/broadcasts` with the sender resolved from `from`. A repeated `idempotencyKey` answers `409 conflict` with the original in `details.campaign`. A `channel` the resolved sender does not carry (`smart`: no channel at all) is refused at creation with `409 no_channel`, as on `POST /v2/messages`, rather than created and failed at send. `from` with a `channel` it does not carry (a `sms` campaign `from: "sms_oneway"`) is that same `409 no_channel`, in the words `POST /v2/messages` uses (`details.connected`, and `sms` vs `sms_oneway` told apart); `channel` only picks between senders that share an address, it never makes an existing address `404`.

## Body

- `name` (string, required): Name of the broadcast campaign.
- `channel` (string, required): Broadcast delivery channel. Use 'smart' for per-contact intelligent routing. One of: `smart`, `sms`, `sms_oneway`, `whatsapp`, `telegram`, `email`.
- `messageType` (string): Type of message for broadcast. One of: `text`, `image`, `video`, `audio`, `document`, `template`.
- `text` (string): Text content or caption. Supports template variables: {{name}}, {{1}}, etc.
- `content` (object): Content for non-text broadcast message types.
  - `mediaUrl` (string): URL of the media file.
  - `mediaId` (string): Media ID if already uploaded.
  - `mimeType` (string): MIME type of the media.
  - `filename` (string): Filename for documents.
  - `templateId` (string): Template ID for template messages.
  - `templateVariables` (object): Default body variables (can be overridden per contact). Key them to match the template body: by position (`1`, `2`, ...) for positional templates, or by name (e.g. `customer_name`) for named templates. Zavu detects the template's format and sends the correct payload to Meta. Do not mix positional and named keys.
    - `{key}` (string)
  - `templateButtonVariables` (object): Default button variables for dynamic URL/OTP buttons. Keys are the button index (0, 1, 2). Per-contact values override these.
    - `{key}` (string)
  - `templateHeaderVariables` (object): Default value for a text-header variable, keyed by `1` (can be overridden per contact). If omitted, Zavu resolves the header from `templateVariables` by the header placeholder's name.
    - `{key}` (string)
- `emailSubject` (string): Email subject line. Required for email broadcasts.
- `emailHtmlBody` (string): HTML body for email broadcasts.
- `scheduledAt` (string<date-time>): Schedule the broadcast for future delivery.
- `metadata` (object)
  - `{key}` (string)
- `idempotencyKey` (string): Idempotency key to prevent duplicate broadcasts.
- `from` (string): The address that sends the campaign (`GET /v2/channels`). The project default when omitted. Resolved like `from` on `POST /v2/messages`, with the campaign's `channel` as the transport (none for `smart`).

## Responses

### 201

Created as `draft`.

- `campaign` (object, required): A campaign. The same object as a /v1 broadcast: its `id` works on `/v1/broadcasts/{broadcastId}`.
  - `id` (string, required)
  - `name` (string, required)
  - `status` (string, required): Current status of the campaign. `pending_review`: in the automated content review. `pending_admin_review`: the automated review passed and it waits for a person, which is where every channel but a WhatsApp campaign on an approved template goes. `rejected`: the review refused it; edit it and send again, or escalate. `scheduled` and `sending` follow approval. One of: `draft`, `pending_review`, `pending_admin_review`, `approved`, `rejected`, `escalated`, `rejected_final`, `scheduled`, `sending`, `paused`, `completed`, `cancelled`, `failed`.
  - `channel` (string, required): Broadcast delivery channel. Use 'smart' for per-contact intelligent routing. One of: `smart`, `sms`, `sms_oneway`, `whatsapp`, `telegram`, `email`.
  - `messageType` (string, required): Type of message for broadcast. One of: `text`, `image`, `video`, `audio`, `document`, `template`.
  - `text` (string)
  - `content` (object): Content for non-text broadcast message types.
    - `mediaUrl` (string): URL of the media file.
    - `mediaId` (string): Media ID if already uploaded.
    - `mimeType` (string): MIME type of the media.
    - `filename` (string): Filename for documents.
    - `templateId` (string): Template ID for template messages.
    - `templateVariables` (object): Default body variables (can be overridden per contact). Key them to match the template body: by position (`1`, `2`, ...) for positional templates, or by name (e.g. `customer_name`) for named templates. Zavu detects the template's format and sends the correct payload to Meta. Do not mix positional and named keys.
      - `{key}` (string)
    - `templateButtonVariables` (object): Default button variables for dynamic URL/OTP buttons. Keys are the button index (0, 1, 2). Per-contact values override these.
      - `{key}` (string)
    - `templateHeaderVariables` (object): Default value for a text-header variable, keyed by `1` (can be overridden per contact). If omitted, Zavu resolves the header from `templateVariables` by the header placeholder's name.
      - `{key}` (string)
  - `emailSubject` (string)
  - `totalContacts` (integer, required): Total number of contacts in the broadcast.
  - `pendingCount` (integer)
  - `sendingCount` (integer)
  - `sentCount` (integer): Recipients whose message the provider accepted, without a confirmed delivery yet. Channels that never report delivery keep their recipients here.
  - `deliveredCount` (integer): Recipients with confirmed delivery to the device.
  - `failedCount` (integer)
  - `estimatedCost` (number, nullable): Estimated total cost in USD.
  - `reservedAmount` (number, nullable): Amount reserved from balance in USD.
  - `actualCost` (number, nullable): Actual cost so far in USD.
  - `scheduledAt` (string<date-time>)
  - `startedAt` (string<date-time>)
  - `completedAt` (string<date-time>)
  - `metadata` (object)
    - `{key}` (string)
  - `createdAt` (string<date-time>, required)
  - `updatedAt` (string<date-time>)
  - `reviewResult` (object, nullable): AI content review result.
    - `score` (number): Content safety score from 0.0 to 1.0, where 1.0 is completely safe.
    - `categories` (string[]): Policy categories violated, if any.
    - `reasoning` (string): Explanation of the review decision.
    - `reviewedAt` (string<date-time>)
    - `flaggedContent` (string[], nullable): Problematic text fragments, if any.
  - `reviewAttempts` (integer, nullable): Number of review attempts (max 3).
  - `from` (string, nullable): The address the campaign is sent from, for its `channel` (the sender's first address when `channel` is `smart`).

### 400

Invalid body (`details.errors`).

- `code` (string, required)
- `message` (string, required)
- `details` (object): Machine-readable context. Keys that mean the same thing everywhere: - `errors`: present on every `400 invalid_request` for an invalid body or query parameter, `[{path, message, ...}]`, `path` naming the field (`["to"]`, `["contacts", 2, "recipient"]`, `["limit"]`). - `recommendation`: one sentence saying what to do next. - `dashboardUrl`: the dashboard screen that fixes it. - `connect`: on `409 no_channel`, the channels this project can connect to reach the recipient, each `{type, request, note, available, dashboardUrl?}`; `request` is the exact call (`POST /v2/channels` with its body, or `POST /v2/numbers`), or null for a channel connected from the dashboard (WhatsApp, Instagram, Messenger). `available: false` says why in `note` (one-way SMS closed until business verification is approved). - `address` / `from`: the address the error is about, under the name the request used for it.
  - `errors` (object[])
    - `path` (object[], required)
    - `message` (string, required)
    - `code` (string)

### 401

Unauthorized.

- `code` (string, required)
- `message` (string, required)
- `details` (object): Machine-readable context. Keys that mean the same thing everywhere: - `errors`: present on every `400 invalid_request` for an invalid body or query parameter, `[{path, message, ...}]`, `path` naming the field (`["to"]`, `["contacts", 2, "recipient"]`, `["limit"]`). - `recommendation`: one sentence saying what to do next. - `dashboardUrl`: the dashboard screen that fixes it. - `connect`: on `409 no_channel`, the channels this project can connect to reach the recipient, each `{type, request, note, available, dashboardUrl?}`; `request` is the exact call (`POST /v2/channels` with its body, or `POST /v2/numbers`), or null for a channel connected from the dashboard (WhatsApp, Instagram, Messenger). `available: false` says why in `note` (one-way SMS closed until business verification is approved). - `address` / `from`: the address the error is about, under the name the request used for it.
  - `errors` (object[])
    - `path` (object[], required)
    - `message` (string, required)
    - `code` (string)

### 403

`kyb_required`: `channel: "sms_oneway"` on an account required to verify its business before using one-way SMS, until that verification is approved. One-way SMS needs no verification by default.

- `code` (string, required)
- `message` (string, required)
- `details` (object): Machine-readable context. Keys that mean the same thing everywhere: - `errors`: present on every `400 invalid_request` for an invalid body or query parameter, `[{path, message, ...}]`, `path` naming the field (`["to"]`, `["contacts", 2, "recipient"]`, `["limit"]`). - `recommendation`: one sentence saying what to do next. - `dashboardUrl`: the dashboard screen that fixes it. - `connect`: on `409 no_channel`, the channels this project can connect to reach the recipient, each `{type, request, note, available, dashboardUrl?}`; `request` is the exact call (`POST /v2/channels` with its body, or `POST /v2/numbers`), or null for a channel connected from the dashboard (WhatsApp, Instagram, Messenger). `available: false` says why in `note` (one-way SMS closed until business verification is approved). - `address` / `from`: the address the error is about, under the name the request used for it.
  - `errors` (object[])
    - `path` (object[], required)
    - `message` (string, required)
    - `code` (string)

### 404

`from` names no address of this project (`details.from`).

- `code` (string, required)
- `message` (string, required)
- `details` (object): Machine-readable context. Keys that mean the same thing everywhere: - `errors`: present on every `400 invalid_request` for an invalid body or query parameter, `[{path, message, ...}]`, `path` naming the field (`["to"]`, `["contacts", 2, "recipient"]`, `["limit"]`). - `recommendation`: one sentence saying what to do next. - `dashboardUrl`: the dashboard screen that fixes it. - `connect`: on `409 no_channel`, the channels this project can connect to reach the recipient, each `{type, request, note, available, dashboardUrl?}`; `request` is the exact call (`POST /v2/channels` with its body, or `POST /v2/numbers`), or null for a channel connected from the dashboard (WhatsApp, Instagram, Messenger). `available: false` says why in `note` (one-way SMS closed until business verification is approved). - `address` / `from`: the address the error is about, under the name the request used for it.
  - `errors` (object[])
    - `path` (object[], required)
    - `message` (string, required)
    - `code` (string)

### 409

`no_channel`: no `from` and no default, or the sender has no `channel` transport (`details.channel`, `details.connect`, `details.connected`, the same answer as `POST /v2/messages` with that `channel`). `ambiguous_from`, or `conflict` (repeated `idempotencyKey`; the original is in `details.campaign`).

- `code` (string, required)
- `message` (string, required)
- `details` (object): Machine-readable context. Keys that mean the same thing everywhere: - `errors`: present on every `400 invalid_request` for an invalid body or query parameter, `[{path, message, ...}]`, `path` naming the field (`["to"]`, `["contacts", 2, "recipient"]`, `["limit"]`). - `recommendation`: one sentence saying what to do next. - `dashboardUrl`: the dashboard screen that fixes it. - `connect`: on `409 no_channel`, the channels this project can connect to reach the recipient, each `{type, request, note, available, dashboardUrl?}`; `request` is the exact call (`POST /v2/channels` with its body, or `POST /v2/numbers`), or null for a channel connected from the dashboard (WhatsApp, Instagram, Messenger). `available: false` says why in `note` (one-way SMS closed until business verification is approved). - `address` / `from`: the address the error is about, under the name the request used for it.
  - `errors` (object[])
    - `path` (object[], required)
    - `message` (string, required)
    - `code` (string)

## Example

```bash
curl -X POST "https://api.zavu.dev/v2/campaigns" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Black Friday Sale",
  "channel": "smart"
}'
```

```typescript
import Zavudev from "@zavudev/sdk";

const zavu = new Zavudev({ apiKey: process.env.ZAVUDEV_API_KEY });

const result = await zavu.v2.campaigns.create({
  name: "Black Friday Sale",
  channel: "smart",
});

console.log(result);
```
