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

# Send Bulk Messages

> Send multiple SMS messages in a single bulk request.

Send up to 1,000 SMS messages in a single API call. Each message in the batch is validated independently -- successfully validated messages are queued even if others in the batch fail validation.

## Body Parameters

<ParamField body="messages" type="array" required>
  Array of message objects (maximum 1,000 per request). Each object supports the following fields:

  <Expandable title="Message object fields">
    <ParamField body="to" type="string" required>
      Recipient phone number in E.164 format (e.g. `+14155551234`).
    </ParamField>

    <ParamField body="list_number" type="string" required>
      Sending list phone number in E.164 format. Must belong to your account.
    </ParamField>

    <ParamField body="body" type="string" required>
      Message body text. Use the `{{messageId}}` placeholder to insert the unique message ID into the body at send time.
    </ParamField>

    <ParamField body="wrap_links" type="boolean" default="false">
      Intended to auto-shorten URLs in the message body for click tracking, but this is **not currently active** — raw URLs are sent unshortened. For click-tracked links, create the link via the [Shorten Link](/api-reference/v2/links/shorten-link) endpoint and put the returned short URL in your body.
    </ParamField>

    <ParamField body="contact_data" type="object">
      Optional per-contact data for macro substitution. Supports `first_name` (string), `last_name` (string), `custom_fields` (object), and `timezone` (string).
    </ParamField>

    <ParamField body="metadata" type="object">
      Optional key-value metadata dictionary. **Currently accepted but not stored or returned** — the endpoint reads this field and then discards it, so it cannot yet be used for correlation.
    </ParamField>
  </Expandable>
</ParamField>

## Response Fields

<ResponseField name="queued_count" type="integer">
  Number of messages successfully queued for delivery.
</ResponseField>

<ResponseField name="error_count" type="integer">
  Number of messages that failed validation or queueing.
</ResponseField>

<ResponseField name="errors" type="array">
  Array of error objects for messages that failed validation or queueing. Each object contains:

  <Expandable title="Error object fields">
    <ResponseField name="index" type="integer">
      Zero-based index of the failed message in the original `messages` array.
    </ResponseField>

    <ResponseField name="to" type="string">
      The recipient phone number from the failed message, if provided.
    </ResponseField>

    <ResponseField name="code" type="string">
      Machine-readable error code identifying the failure.
    </ResponseField>

    <ResponseField name="error" type="string">
      Human-readable error description.
    </ResponseField>

    <ResponseField name="errorCode" type="string">
      Present only when `code` is `kafka_producer_failure`. Constant value `KAFKA_PRODUCER_FAILURE`, indicating the message passed validation but could not be published to the delivery queue.
    </ResponseField>

    <ResponseField name="message_id" type="string">
      Present only when `code` is `kafka_producer_failure`. The message ID that was assigned before the publish failure.
    </ResponseField>
  </Expandable>
</ResponseField>

## Examples

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.tracklysms.com/api/v2/send/bulk \
    -H "X-Api-Key: trk_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "messages": [
        {
          "to": "+14155551234",
          "list_number": "+18005551000",
          "body": "Your promo code is SAVE20. Ref: {{messageId}}"
        },
        {
          "to": "+14155559876",
          "list_number": "+18005551000",
          "body": "Your promo code is SAVE20. Ref: {{messageId}}"
        }
      ]
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.tracklysms.com/api/v2/send/bulk",
      headers={
          "X-Api-Key": "trk_your_api_key_here",
          "Content-Type": "application/json",
      },
      json={
          "messages": [
              {
                  "to": "+14155551234",
                  "list_number": "+18005551000",
                  "body": "Your promo code is SAVE20. Ref: {{messageId}}",
              },
              {
                  "to": "+14155559876",
                  "list_number": "+18005551000",
                  "body": "Your promo code is SAVE20. Ref: {{messageId}}",
              },
          ]
      },
  )

  data = response.json()
  print(f"Queued: {data['queued_count']}, Errors: {data['error_count']}")
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.tracklysms.com/api/v2/send/bulk", {
    method: "POST",
    headers: {
      "X-Api-Key": "trk_your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      messages: [
        {
          to: "+14155551234",
          list_number: "+18005551000",
          body: "Your promo code is SAVE20. Ref: {{messageId}}",
        },
        {
          to: "+14155559876",
          list_number: "+18005551000",
          body: "Your promo code is SAVE20. Ref: {{messageId}}",
        },
      ],
    }),
  });

  const data = await response.json();
  console.log(`Queued: ${data.queued_count}, Errors: ${data.error_count}`);
  ```
</RequestExample>

<ResponseExample>
  ```json Success (201) theme={null}
  {
    "queued_count": 2,
    "error_count": 0,
    "errors": []
  }
  ```

  ```json Partial Success (201) theme={null}
  {
    "queued_count": 1,
    "error_count": 1,
    "errors": [
      {
        "index": 1,
        "to": "not-a-number",
        "code": "invalid_phone",
        "error": "Invalid recipient phone number format"
      }
    ]
  }
  ```

  ```json Error (400) theme={null}
  {
    "error": "messages array is required",
    "code": "missing_messages"
  }
  ```

  ```json Error (413) theme={null}
  {
    "error": "Maximum 1000 messages per request",
    "code": "too_many_messages"
  }
  ```
</ResponseExample>

## Error Codes

### Request-Level Errors

| HTTP Status | Error Code          | Description                                            |
| ----------- | ------------------- | ------------------------------------------------------ |
| 400         | `missing_messages`  | The `messages` array is required but was not provided. |
| 413         | `too_many_messages` | The `messages` array exceeds the 1,000 message limit.  |

Individual messages that fail are reported per-message (below) and the request still returns `201`. The request returns `502` only when **every** message fails to queue for delivery. Authenticated requests can also fail with `401 invalid_credentials`, `403 account_suspended`, or `429 rate_limited` — see [Error Codes](/api-reference/v2/error-codes).

### Per-Message Errors

Returned inside each failed entry of the response's `errors[]` array (as `code`), with the request status `201`.

| Code                     | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| `missing_to`             | The `to` field is required but was not provided.                                           |
| `missing_list_number`    | The `list_number` field is required but was not provided.                                  |
| `missing_body`           | The `body` field is required but was not provided.                                         |
| `invalid_phone`          | The `to` field is not a valid E.164 phone number.                                          |
| `invalid_list_number`    | The `list_number` field is not a valid E.164 phone number.                                 |
| `list_not_found`         | The sending list was not found or does not belong to your account.                         |
| `webhook_not_configured` | Webhook verification required before sending (BYOC lists).                                 |
| `pending_confirmation`   | The recipient has not confirmed their double opt-in yet, so they cannot be messaged.       |
| `doi_expired`            | The recipient never confirmed their double opt-in and the confirmation window has expired. |
| `warmup_limit`           | The recipient is not yet eligible to be messaged under the list's warm-up schedule.        |
| `kafka_producer_failure` | The message could not be queued for delivery (transient); retry the failed records.        |

## Next Steps

<CardGroup cols={2}>
  <Card title="Campaign Execution" icon="rocket" href="/guides/campaigns/execution">
    How messages flow from queue to delivery
  </Card>

  <Card title="Create Contact" icon="user-plus" href="/api-reference/v2/contacts/create-contact">
    Add contacts before sending
  </Card>
</CardGroup>
