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

# Batch Sending

> Send up to 1,000 email messages in a single API request — batch request anatomy, partial-failure handling, idempotency, rate limits, and patterns for high-volume sending.

Send many messages in one API call with `POST /email_messages/batch`. Each message in the batch is validated and processed independently — one bad message never blocks the rest — and up to **1,000 messages** can be sent per request.

Batch sending is the right tool when you generate many distinct messages at once: a marketing campaign with per-recipient personalization, a batch of transactional notifications (invoices, receipts, shipping updates), or a nightly digest job. If you're sending the same content to many recipients, a single regular send with multiple `to` entries is simpler.

<Callout type="info">
  This guide assumes you have a Telnyx account with an API key and a sender your account is permitted to send from — normally a verified sending domain, or the shared domain during onboarding. If you don't, complete the [Quickstart](/docs/messaging/email/quickstart) first, then return here. Replace `YOUR_API_KEY` and the sender and recipient addresses in the examples with your own.
</Callout>

```bash curl theme={null}
curl -X POST https://api.telnyx.com/v2/email_messages/batch \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "messages": [
      {
        "from": "sender@example.com",
        "to": ["recipient1@example.com"],
        "subject": "Hello 1",
        "text_body": "Message 1"
      },
      {
        "from": "sender@example.com",
        "to": ["recipient2@example.com"],
        "subject": "Hello 2",
        "text_body": "Message 2"
      }
    ]
  }'
```

## Limits

| Limit                                                      | Value | On exceed                                                             |
| ---------------------------------------------------------- | ----- | --------------------------------------------------------------------- |
| Messages per batch request                                 | 1,000 | `400` with code `10015`                                               |
| Idempotency-keyed request, captured at the edge for replay | 20 MB | `413 Payload Too Large` at the edge; unkeyed requests bypass this cap |
| Per-message body (decoded)                                 | 1 MB  | Per-message `422` — the rest of the batch still processes             |
| Per-message total (decoded body + attachments)             | 25 MB | Per-message `422` — the rest of the batch still processes             |

Each item in the `messages` array mirrors the single-send [message schema](https://api.telnyx.com/v2/openapi.json) — same fields, same validation, with `from` and `to` required — except the single-send-only reply and forward threading fields (`in_reply_to_message_id`, `reply_to_all`, and `forward_of_message_id`), which are not accepted on batch items. Templates (`template_id` with `template_variables`), `scheduled_at`, attachments, `headers`, `tags`, `metadata`, and per-item `sandbox_mode` all work as in a single send. The one batch-level addition is `sandbox_mode`, which applies sandbox mode to every message in the request and overrides any per-message setting.

<Callout type="info">
  Batch requests are processed synchronously — the response reports the outcome of every message. Large batches take proportionally longer to process; the request timeout at the edge is 55 seconds. If you're near the 1,000-message ceiling, keep payloads small, or split very heavy sends into multiple batch requests.
</Callout>

<Callout type="warning">
  The 20 MB edge cap applies only to requests carrying an `Idempotency-Key`. It is a cap on the **encoded request body**, while the per-message limits are decoded — and the two ceilings are independent. Even a single attachment-heavy message that is valid per-message (up to 25 MB decoded) can expand past 20 MB on the wire and be rejected at the edge on a keyed request. An unkeyed request bypasses the edge cap entirely, but then has no replay protection.
</Callout>

## Response

Every batch response uses `207 Multi-Status`, even when all messages succeed. The response contains a `data` array for created messages (which may be empty), an `errors` array for failed messages, and a `meta` summary:

```json theme={null}
{
  "data": [
    {
      "record_type": "email_message",
      "id": "11111111-1111-1111-1111-111111111111",
      "status": "queued"
    }
  ],
  "errors": [
    {
      "index": 1,
      "code": "bad_request",
      "message": "from, to, and subject are required"
    }
  ],
  "meta": {
    "total": 2,
    "succeeded": 1,
    "failed": 1
  }
}
```

Each entry in `errors` carries the zero-based `index` of the failed message in your request array, a `code`, and a `message`. Fix the failed messages and re-send only those — the successful messages in `data` are already queued and need no re-submission.

Batch item error codes: `bad_request`, `unprocessable_entity`, `not_found`, `forbidden`, `service_unavailable`, `validation_error`, `recipient_suppressed`, and `reputation_suspended`. A per-message size violation (`unprocessable_entity`) fails only that message; the rest of the batch still processes. For the full per-code reference, see [Error Codes](/docs/messaging/email/error-codes#batch-specific-errors).

<Callout type="info">
  Suppression is per-recipient, not per-message. A message whose recipients are all suppressed returns the `recipient_suppressed` per-item error; other messages in the batch are unaffected. See [Suppressions](/docs/messaging/email/suppressions).
</Callout>

## Idempotency

Pass an `Idempotency-Key` HTTP header to safely retry an entire batch. Generate a unique UUID v4 for each logical batch request and reuse the same key only when retrying the identical request body:

```bash curl theme={null}
curl -X POST https://api.telnyx.com/v2/email_messages/batch \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 20dbec69-bc70-4fed-aec7-2a70af37a8a6" \
  -d '{
    "messages": [
      {
        "from": "sender@example.com",
        "to": ["recipient1@example.com"],
        "subject": "Hello 1",
        "text_body": "Message 1"
      }
    ]
  }'
```

The key applies to the **entire batch request** — one key, one request body, one stored response. Do not add per-message idempotency keys inside `messages`; there is no per-message key surface on the batch endpoint.

If a retried batch replays a stored response, the response includes the `Idempotent-Replayed: true` header. Reusing a key with a different body returns `422` with code `10027`. See [Idempotency](/docs/messaging/email/send-email#idempotency) for the full key lifecycle.

<Callout type="warning">
  A keyed batch larger than 20 MB is rejected at the edge with `413` before reaching the Email API. If your batch needs both replay protection and heavy payloads, keep the encoded request under 20 MB — fewer, smaller messages per request — or send the heavy batch unkeyed and make retry safety your application's responsibility.
</Callout>

## Rate limits

Batch requests are rate-limited **per account** on top of the general request limits. The response carries the Envoy draft-03 rate limit headers — `x-ratelimit-limit`, `x-ratelimit-remaining`, and `x-ratelimit-reset` (seconds until the window resets) — so a well-behaved client can pace itself without guessing:

```http theme={null}
x-ratelimit-limit: 10, 10;w=60
x-ratelimit-remaining: 6
x-ratelimit-reset: 44
```

When the limit is exceeded, the request returns `429 Too Many Requests`. Honor `x-ratelimit-reset` when present and fall back to exponential backoff with jitter when it's not. Exact tiers depend on your account level — a 1M-message-per-day workload averages under one batch request per minute, so the limits rarely bind for steady senders. Contact support to confirm or raise your account's limits.

<Callout type="info">
  Pace your requests with `x-ratelimit-remaining` and `x-ratelimit-reset` rather than firing unbounded parallel batches. If your account's allowance supports concurrency and you need higher sustained throughput than sequential requests deliver, contact support to confirm your limits rather than fanning out against `429`s.
</Callout>

## Patterns

### Personalize at scale with templates

Batch plus [templates](/docs/messaging/email/templates) is the cleanest high-volume pattern: one template, one batch, per-recipient variables.

```bash curl theme={null}
curl -X POST https://api.telnyx.com/v2/email_messages/batch \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "messages": [
      {
        "from": "billing@example.com",
        "to": ["ada@example.com"],
        "template_id": "7a7c1a2b-1111-4c72-8c21-2bbf3d40c123",
        "template_variables": { "first_name": "Ada", "invoice_total": "$42.00" }
      },
      {
        "from": "billing@example.com",
        "to": ["grace@example.com"],
        "template_id": "7a7c1a2b-1111-4c72-8c21-2bbf3d40c123",
        "template_variables": { "first_name": "Grace", "invoice_total": "$17.50" }
      }
    ]
  }'
```

### Schedule a batch

Every message in a batch can carry its own `scheduled_at` — mixed immediate and scheduled sends in one request are fine. See [Schedule a send](/docs/messaging/email/send-email#schedule-a-send).

### Track outcomes per message

Each created message in `data` returns its own `id`. Use those IDs with `GET /email_messages/{id}` and `GET /email_messages/{id}/events` for per-message tracking, or configure [webhooks](/docs/messaging/email/webhooks-events) to receive delivery and engagement events for every message in the batch.

<Callout type="info">
  The batch response's `data` entries are creation records — `status: "queued"` is the starting point, not the delivery outcome. Track delivery through events, exactly as with single sends.
</Callout>

### Retry only the failures

The `errors[].index` mapping makes partial retry mechanical. This complete example sends a batch, then resubmits only the failed messages with a **new** idempotency key:

```python theme={null}
import os
import time
import uuid
import requests

url = "https://api.telnyx.com/v2/email_messages/batch"
headers = {
    "Authorization": f"Bearer {os.environ['TELNYX_API_KEY']}",
    "Content-Type": "application/json",
    "Idempotency-Key": str(uuid.uuid4()),
}

messages = [
    {"from": "sender@example.com", "to": [f"user{i}@example.com"],
     "subject": f"Hello {i}", "text_body": f"Message {i}"}
    for i in range(100)
]

response = requests.post(url, json={"messages": messages}, headers=headers)

if response.status_code == 429:
    # An outer 429 failed the whole request — no messages were processed.
    # Wait out the window, then retry the SAME request with the SAME key.
    wait_seconds = int(response.headers.get("x-ratelimit-reset", "60"))
    time.sleep(wait_seconds + 1)
    response = requests.post(url, json={"messages": messages}, headers=headers)

if response.status_code != 207:
    raise SystemExit(f"Batch request failed: {response.status_code} {response.text}")

# Only a 207 reaches here. Per-item errors are indexed failures of
# individual messages; outer errors were handled above.
result = response.json()
failed_indexes = [e["index"] for e in result.get("errors", [])]

if failed_indexes:
    retry_headers = {**headers, "Idempotency-Key": str(uuid.uuid4())}
    retry_messages = [messages[i] for i in failed_indexes]
    retry = requests.post(url, json={"messages": retry_messages}, headers=retry_headers)
    print(f"Retried {len(retry_messages)} failed messages: {retry.status_code}")
```

## What's next

* [Sending Email](/docs/messaging/email/send-email) — full payload reference for each message
* [Rate Limits & Quotas](/docs/messaging/email/rate-limits) — all size and rate ceilings
* [Templates](/docs/messaging/email/templates) — Liquid templating with per-recipient variables
* [Webhooks & Events](/docs/messaging/email/webhooks-events) — delivery and engagement events for every message
