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

# 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:

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



## OpenAPI

````yaml /api-reference/v2/openapi.json post /v2/campaigns/{campaignId}/recipients
openapi: 3.0.3
info:
  title: Zavu API v2
  version: 2.0.0
  description: >-
    The second version of the Zavu API. It runs on the same engine as /v1: the
    same API keys, the same per-project rate limit, the same sending limits,
    prices and reviews. /v1 keeps working unchanged, and you can mix calls to
    both.


    **Addresses, not senders.** /v2 has no sender object. Where a message leaves
    from is named the way the recipient sees it: an E.164 number, an email
    address, `@botname` for Telegram, `@username` for Instagram, the Facebook
    Page id for Messenger, and the literal `sms_oneway` for one-way SMS (sent
    from shared numbers, never a number of your own). `GET /v2/channels` lists
    one entry per (address, type); a number that carries WhatsApp and SMS is two
    entries with the same address.


    **What else changes.** No `Zavu-Sender` header (`from` or `conversationId`
    instead); every send reports how it was routed; one project webhook carries
    every event; every error is `{code, message, details}` at the root.


    **Wrong method.** A path under `/v2/messages`, `/v2/conversations`,
    `/v2/contacts`, `/v2/channels`, `/v2/templates`, `/v2/campaigns`,
    `/v2/agents`, `/v2/webhook` or `/v2/email-domains` called with a method it
    does not take answers `405 method_not_allowed`, with `details.allowed` and
    an `Allow` header.


    **Money.** Every amount is USD, rounded to 6 decimals.
servers:
  - url: https://api.zavu.dev
security:
  - bearerAuth: []
tags:
  - name: Messages
    description: Send, list and read messages.
  - name: Conversations
    description: Inbox threads, one per contact across channels.
  - name: Contacts
    description: Contacts and the identifiers they are reached at.
  - name: Channels
    description: 'Where the project can send from: one entry per (address, type).'
  - name: Templates
    description: WhatsApp message templates.
  - name: Campaigns
    description: One message to many recipients.
  - name: Webhook
    description: The one project webhook for every event.
  - name: Agents
    description: AI agents and the addresses they answer on.
  - name: Numbers
    description: Search and buy phone numbers.
  - name: Email Domains
    description: Domains to send email from.
  - name: Functions
    description: Configure Zavu Functions.
paths:
  /v2/campaigns/{campaignId}/recipients:
    post:
      tags:
        - Campaigns
      summary: Add recipients
      description: >-
        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`.
      operationId: addCampaignRecipientsV2
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID. The same ID works as `broadcastId` on /v1.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BroadcastContactsAddRequest'
      responses:
        '201':
          description: >-
            Processed. `added` + `duplicates` + `invalid` equals the number
            sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastContactsAddResponse'
        '400':
          description: >-
            A malformed recipient, or bare digits on a phone campaign
            (`details.errors`, with `suggestion`); nothing added.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Campaign not found, or it belongs to another project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`invalid_status`: the campaign is not a draft (`details.status`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
components:
  schemas:
    BroadcastContactsAddRequest:
      type: object
      required:
        - contacts
      properties:
        contacts:
          type: array
          description: List of contacts to add (max 1000 per request).
          maxItems: 1000
          items:
            $ref: '#/components/schemas/BroadcastContactInput'
    BroadcastContactsAddResponse:
      type: object
      required:
        - added
        - duplicates
        - invalid
      properties:
        added:
          type: integer
          description: Number of contacts successfully added.
        duplicates:
          type: integer
          description: Number of duplicate contacts skipped.
        invalid:
          type: integer
          description: Number of invalid contacts rejected.
        errors:
          type: array
          description: Details about invalid contacts.
          items:
            type: object
            properties:
              recipient:
                type: string
              reason:
                type: string
                description: >-
                  Why it was not added, in words that are true of this recipient
                  and this campaign.
              code:
                type: string
                enum:
                  - unreachable_by_channel
                  - ten_dlc_required
                description: >-
                  Set for `unreachable_by_channel` and `ten_dlc_required`.
                  Absent on other reasons.
    Error:
      type: object
      required:
        - code
        - message
      description: >-
        Every v2 error has this shape at the root. There is no `{error: {...}}`
        wrapper anywhere in v2. A missing resource is `404 not_found` with the
        message `<Resource> not found`; an address the project does not have is
        `404 not_found`, `None of this project's channels has the address
        <address>.`, with the address in `details` under the name the request
        used (`from` or `address`). A `message` is written for the caller and
        never carries a stack trace or a source path. A body that is not JSON is
        `400 invalid_json`, with `details.errors` (`path: []`, the parse error
        in `message`).
      properties:
        code:
          type: string
          example: invalid_request
        message:
          type: string
        details:
          type: object
          additionalProperties: true
          description: >-
            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.
          properties:
            errors:
              type: array
              items:
                type: object
                required:
                  - path
                  - message
                properties:
                  path:
                    type: array
                    items:
                      oneOf:
                        - type: string
                        - type: integer
                  message:
                    type: string
                  code:
                    type: string
                additionalProperties: true
    BroadcastContactInput:
      type: object
      required:
        - recipient
      properties:
        recipient:
          type: string
          description: >-
            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.
          example: '+14155551234'
        templateVariables:
          type: object
          description: >-
            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.
          additionalProperties:
            type: string
          example:
            '1': John
            '2': ORD-12345
        templateButtonVariables:
          type: object
          description: >-
            Per-contact button variables for dynamic URL/OTP buttons. Keys are
            the button index (0, 1, 2).
          additionalProperties:
            type: string
          example:
            '0': abc-report-token
        templateHeaderVariables:
          type: object
          description: >-
            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.
          additionalProperties:
            type: string
          example:
            '1': Jorge y Laura
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.