# Add recipients

> Up to 1,000 per call. Recipients are held to the rules POST /v2/messages holds to to. Validation happens in two steps with different outcomes:

`POST https://api.zavu.dev/v2/campaigns/{campaignId}/recipients`

Up to 1,000 per call. Recipients are held to the rules `POST /v2/messages` holds `to` to. Validation happens in two steps with different outcomes:

1. **The request as a whole.** A phone number written with spaces, dashes, dots or parentheses after its `+` is read as the E.164 number first. Then a recipient that is not a phone number, email address, BSUID or chat id makes the whole request `400`, with `details.errors[].path` naming it (`["contacts", 2, "recipient"]`); so do 8-15 bare digits on an `sms`, `sms_oneway`, `whatsapp`, `voice` or `smart` campaign, with `details.errors[].suggestion` (`+<digits>`), as a send refuses them. Nothing is added.
2. **Each recipient against the campaign.** A well-formed recipient the campaign cannot reach is skipped and reported in `invalid` and `errors[{recipient, reason, code?}]`: `code: "unreachable_by_channel"` for an email address on a phone campaign or a phone number on an email campaign; `code: "ten_dlc_required"` for a US/Canadian (+1) number on an `sms` or `sms_oneway` campaign while this project has no approved 10DLC campaign (the send would refuse it, as `POST /v2/messages` answers `403 ten_dlc_required`). Repeats of a recipient already in the campaign or in the same call are skipped as `duplicates`. The others are added, and the call answers `201`.

So `201` can mean some were not added: check `invalid`. A campaign that is not a draft answers `409 invalid_status`.

## Parameters

- `campaignId` (path, string, required): Campaign ID. The same ID works as `broadcastId` on /v1.

## Body

- `contacts` (object[], required): List of contacts to add (max 1000 per request).
  - `recipient` (string, required): Phone number (E.164) or email address. On a `telegram` broadcast, the numeric chat ID of someone who has already written to your bot (`838360395`, or `telegram:838360395`); a phone number is rejected there, because a bot cannot start a chat.
  - `templateVariables` (object): Per-contact body variables. 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): Per-contact button variables for dynamic URL/OTP buttons. Keys are the button index (0, 1, 2).
    - `{key}` (string)
  - `templateHeaderVariables` (object): Per-contact value for a text-header variable, keyed by `1`. If omitted, Zavu resolves the header from `templateVariables` by the header placeholder's name.
    - `{key}` (string)

## Responses

### 201

Processed. `added` + `duplicates` + `invalid` equals the number sent.

- `added` (integer, required): Number of contacts successfully added.
- `duplicates` (integer, required): Number of duplicate contacts skipped.
- `invalid` (integer, required): Number of invalid contacts rejected.
- `errors` (object[]): Details about invalid contacts.
  - `recipient` (string)
  - `reason` (string): Why it was not added, in words that are true of this recipient and this campaign.
  - `code` (string): Set for `unreachable_by_channel` and `ten_dlc_required`. Absent on other reasons. One of: `unreachable_by_channel`, `ten_dlc_required`.

### 400

A malformed recipient, or bare digits on a phone campaign (`details.errors`, with `suggestion`); nothing added.

- `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)

### 404

Campaign not found, or it belongs to another project.

- `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

`invalid_status`: the campaign is not a draft (`details.status`).

- `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/campaign_abc123/recipients" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "contacts": [
    {
      "recipient": "+14155551234"
    }
  ]
}'
```

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

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

const result = await zavu.v2.campaigns.recipients.add("campaign_abc123", {
  contacts: [
    {
      recipient: "+14155551234",
    },
  ],
});

console.log(result);
```
