Skip to main content
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.
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 first, then return here. Replace YOUR_API_KEY and the sender and recipient addresses in the examples with your own.
curl

Limits

Each item in the messages array mirrors the single-send message schema — 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.
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.
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.

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

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:
curl
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 for the full key lifecycle.
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.

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:
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.
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 429s.

Patterns

Personalize at scale with templates

Batch plus templates is the cleanest high-volume pattern: one template, one batch, per-recipient variables.
curl

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.

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 to receive delivery and engagement events for every message in the batch.
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.

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:

What’s next