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

# Bulk template sending on WhatsApp

> Send an approved WhatsApp template to several contacts in a single request

`POST /v1/messages/send-bulk-template` sends an approved WhatsApp template to a list of
contacts at once. Because they're templates, these sends work **outside the 24-hour window**: that's
the way to start a conversation with someone who hasn't talked to you recently.

**Scope:** `messages:create`.

<Note>
  The template must be **approved on Meta** beforehand. This call triggers an existing template;
  it doesn't create or submit templates for approval.
</Note>

## Request fields

| Field          | Type   | Required    | Description                                                                             |
| -------------- | ------ | ----------- | --------------------------------------------------------------------------------------- |
| `inboxId`      | string | Yes         | Inbox (WhatsApp connection) that sends the messages.                                    |
| `templateName` | string | Yes         | Template name as registered in WhatsApp Business.                                       |
| `languageCode` | string | No          | Format `pt_BR`, `en_US`, or just `en`. Default: `pt_BR`.                                |
| `components`   | array  | No          | Dynamic template parameters (see below).                                                |
| `contacts`     | array  | Conditional | List of `{ name, phone }`. Required if you don't send a file.                           |
| `file`         | file   | Conditional | CSV or XLSX with the `name` and `phone` columns. Required if you don't send `contacts`. |

`phone` needs the country code, with no symbols: `5511999999999`. `name` can't be empty.

## Send to a list of contacts

```bash cURL theme={null}
curl -X POST https://api.nuvia.ai/v1/messages/send-bulk-template \
  -H "Authorization: Bearer $NUVIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inboxId": "INBOX_ID",
    "templateName": "welcome",
    "languageCode": "pt_BR",
    "components": [
      { "type": "body", "parameters": [{ "type": "text", "text": "John" }] }
    ],
    "contacts": [
      { "name": "John Smith", "phone": "5511999999999" },
      { "name": "Mary Jones", "phone": "5511888888888" }
    ]
  }'
```

For large lists, send a file instead of the array: `multipart/form-data` with the CSV or XLSX in the
`file` field and the remaining fields in the form. The file needs the `name` and `phone` columns:

```csv theme={null}
name,phone
John Smith,5511999999999
Mary Jones,5511888888888
```

<Note>
  Combining a **file** with dynamic `components` parameters is a case worth testing with a
  few contacts before using it in production. If the parameters aren't applied as expected,
  contact support.
</Note>

## Template parameters

An approved template has placeholders (`{{1}}`, `{{2}}`) and blocks: header, body, and buttons. You
fill this in via `components`, and the number and type of parameters must **match exactly** with the
approved template, otherwise Meta rejects the send.

| Block (`type`) | What it accepts in `parameters`                      |
| -------------- | ---------------------------------------------------- |
| `header`       | `text`, `image`, `video`, `document`                 |
| `body`         | `text`, `currency`, `date_time`                      |
| `button`       | `text` (dynamic URL button), `payload` (quick reply) |

Parameter types:

| `type`      | Fields                               | Example                                                                                    |
| ----------- | ------------------------------------ | ------------------------------------------------------------------------------------------ |
| `text`      | `text`                               | `{ "type": "text", "text": "John" }`                                                       |
| `image`     | `image.link`                         | `{ "type": "image", "image": { "link": "https://..." } }`                                  |
| `video`     | `video.link`                         | `{ "type": "video", "video": { "link": "https://..." } }`                                  |
| `document`  | `document.link`, `document.filename` | `{ "type": "document", "document": { "link": "https://...", "filename": "invoice.pdf" } }` |
| `currency`  | `currencyCode`, `currencyAmount`     | `{ "type": "currency", "currencyCode": "BRL", "currencyAmount": 150.5 }`                   |
| `date_time` | `dateTime` (ISO 8601)                | `{ "type": "date_time", "dateTime": "2026-01-31T23:59:59Z" }`                              |
| `payload`   | `payload`                            | `{ "type": "payload", "payload": "product_1" }`                                            |

<Tip>
  Image, video, and document go in the header via a **public HTTPS URL**. You don't need to
  upload a file or prepare anything beforehand. The URL just needs to be reachable by Meta.
</Tip>

### Examples by template type

<AccordionGroup>
  <Accordion title="Header with image" icon="image">
    Template: image header + `Hello {{1}}! Check out our {{2}} sale.`

    ```json theme={null}
    "components": [
      {
        "type": "header",
        "parameters": [
          { "type": "image", "image": { "link": "https://example.com/promo.jpg" } }
        ]
      },
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "John" },
          { "type": "text", "text": "Black Friday" }
        ]
      }
    ]
    ```
  </Accordion>

  <Accordion title="Button with dynamic URL" icon="link">
    Template: `Your order #{{1}} has been confirmed.` + a button pointing to `.../track/{{1}}`.

    The button's `index` is a **string** and starts at `"0"`:

    ```json theme={null}
    "components": [
      { "type": "body", "parameters": [{ "type": "text", "text": "12345" }] },
      {
        "type": "button",
        "sub_type": "url",
        "index": "0",
        "parameters": [{ "type": "text", "text": "12345" }]
      }
    ]
    ```
  </Accordion>

  <Accordion title="Document in the header" icon="file-pdf">
    Template: document header + `Hello {{1}}, here's your invoice.`

    ```json theme={null}
    "components": [
      {
        "type": "header",
        "parameters": [
          {
            "type": "document",
            "document": {
              "link": "https://example.com/invoice.pdf",
              "filename": "january_invoice.pdf"
            }
          }
        ]
      },
      { "type": "body", "parameters": [{ "type": "text", "text": "John Smith" }] }
    ]
    ```
  </Accordion>

  <Accordion title="Currency amount and date" icon="money-bill">
    Template: `Your {{1}} invoice for {{2}} is due on {{3}}.`

    ```json theme={null}
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "January/2026" },
          { "type": "currency", "currencyCode": "BRL", "currencyAmount": 150.5 },
          { "type": "date_time", "dateTime": "2026-01-31T23:59:59Z" }
        ]
      }
    ]
    ```

    The final formatting (`R$ 150.50`, `01/31/2026`) is done by WhatsApp, based on the recipient's
    device language.
  </Accordion>
</AccordionGroup>

**Carousel** templates aren't accepted on this endpoint. Use the individual template send instead.

## The response is partial

Processing is synchronous and an error on one contact **doesn't stop** the others. The response
carries the consolidated result:

```json theme={null}
{
  "totalProcessed": 100,
  "sentCount": 95,
  "errorCount": 5,
  "sentContacts": [
    { "conversationId": "65f1a2b3c4d5e6f7a8b9c0d1", "phoneNumber": "5511999999999" }
  ],
  "errors": [
    { "row": 5, "contact": "John Smith", "error": "Invalid phone number" },
    { "row": 23, "contact": "Mary Jones", "error": "Template not found for this inbox" }
  ]
}
```

<Warning>
  A `200` does **not** mean everyone received it. Always read `errorCount` and `errors`. If you
  ignore this block, send failures go unnoticed. `row` points to the file's row or the
  index in the `contacts` array.
</Warning>

Resending to those who failed is your integration's responsibility: there's no automatic retry.

## Limits worth knowing

* **WhatsApp account tier**: Meta limits the number of unique conversations started per day
  (1,000, 10,000, or 100,000, depending on your account's tier). The send respects this cap: above
  it, sends start to fail.
* **Media size**: image up to 5 MB (JPG, PNG), video up to 16 MB (MP4, 3GPP), document up to
  100 MB (PDF, DOC/DOCX, XLS/XLSX, PPT/PPTX, TXT), audio up to 16 MB.
* **Media links**: must be public HTTPS URLs, reachable by Meta. A URL behind a login
  or an internal network fails.
* **Batch size**: send in batches of up to 1,000 contacts per request.

## Best practices

<Steps>
  <Step title="Test with a single contact">
    Send it to your own number first and check the result on the device. A parameter out of
    order only shows up in the final message.
  </Step>

  <Step title="Normalize phone numbers beforehand">
    Use the international format with no symbols (`5511999999999`). An invalid number becomes a row in
    `errors`, not an exception.
  </Step>

  <Step title="Save the conversationId values">
    `sentContacts` returns the conversation created for each contact: that's how you track the
    reply, via [webhooks](/en/guias/webhooks) or `GET /v1/messages/conversation/{id}`.
  </Step>

  <Step title="Treat errors as a reprocessing queue">
    Resend only the rows in `errors`, after fixing the cause.
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/en/guias/webhooks">
    Receive the replies from those reached by the send.
  </Card>

  <Card title="API reference" icon="code" href="/en/api-reference/messages">
    The full contract for this endpoint and the other sends.
  </Card>
</CardGroup>
