Batch send
Send the same template to many recipients in a single API call, with per-recipient merge variables.
Send the same template to many recipients in a single API call, with per-recipient merge variables. Ideal for digests, receipts and notifications.
POST /v3/messages.batch
The batch endpoint accepts up to 5,000 recipients per call. Each recipient renders independently and produces its own message.id — failures on one recipient never block the others.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Required | Verified sender. Format: "Name <email@domain.com>" or just the address. |
from_name | string | Optional | Display name for the From header, applied to every message in the batch. Overrides a name embedded in from. |
subject | string | Required | Subject line. Supports merge tags from recipients[].vars and global_vars. |
html | string | Optional | HTML body with merge tags. |
text | string | Optional | Plain-text alternative. |
template | string | Optional | Template ID — used in place of html/text. |
recipients | array of objects | Required | Up to 5,000 entries. See Recipient object below. |
global_vars | object | Optional | Merge values shared by every recipient (e.g. campaign-wide URLs, brand name). Per-recipient vars override matching keys. |
tags | array of strings | Optional | Tags applied to every message in the batch (max 10). See Tags. |
headers | object | Optional | Custom X-* headers applied to every message in the batch — e.g. a correlation id like {"X-TES-MSGID": "tes-7f3a9c"}. Each header round-trips end-to-end: stamped on the delivered emails, stored with each message, echoed on every webhook payload, and returned by GET /v3/messages/{id}. Limits: 32 headers, 1 KB per value; names must start with X-, and X-Wemail-* / X-SES-* are reserved. |
webhook_url | string (URL) | Optional | Per-message webhook applied to every message in the batch — each message’s events are also POSTed to this HTTPS URL, in addition to your configured endpoints. |
attachments | array | Optional | File attachments applied to every message in the batch. Each: filename, content (base64), content_type. Limits: 5 MB decoded per file, 20 MB per message (all files combined, decoded) and 30 GB total per send — every recipient receives a full copy, so the total is size × recipients. See Attachments. |
Recipient object
Each entry in recipients describes one rendered message. Per-recipient vars are merged on top of global_vars, so you can keep shared content in one place and only pass what differs.
| Parameter | Type | Required | Description |
|---|---|---|---|
to | string or array of strings | Required | The destination email address, or an array of up to 1,000 addresses. Whitespace is trimmed automatically; an invalid address rejects only this recipient (reported in rejected_recipients), never the whole batch. One entry per recipient is typical — for CC/BCC, send a separate batch entry. |
name | string | Optional | Display name for this recipient. If set, the To header becomes "Name <email>". |
vars | object | Optional | Per-recipient merge variables, merged over global_vars (recipient wins on matching keys). Keys become available as {{key}} in the subject, HTML, and text. |
tags | array of strings | Optional | Extra tags for this single recipient (max 10, 128 chars each), appended after the batch-level tags. Useful for per-cohort segmentation (e.g. "locale:en", "plan:scale"). |
metadata | object | Optional | Free-form key/value pairs stored with this recipient’s message — your internal IDs, A/B variant, etc. Not visible to the recipient. |
send_at | ISO 8601 timestamp | Optional | Schedule this single recipient for future delivery. Must be in the future and at most 1 year out; values outside that window are rejected with 400. |
Responses
| Status | Description |
|---|---|
201 | Created. { batch_id, accepted, rejected, message_ids, rejected_recipients? }. Invalid or unbuildable recipients are counted in rejected and detailed (first 100) in rejected_recipients as { index, to, reason } — index is the zero-based position in your recipients array. |
400 | Structural validation error (more than 5,000 recipients, a recipient to array over 1,000 addresses, attachments over 5 MB per file / 20 MB per message or 30 GB total across all recipients, a send_at in the past or more than 1 year out, or every recipient invalid), or the from domain isn't registered / verified on your account. A malformed individual address is NOT a 400 — it is skipped and reported in rejected_recipients. |
403 | Sandbox send to a recipient not on the confirmed test allow-list, or the account is suspended. |
404 | The template id doesn't exist on your account. |
409 | A request with the same Idempotency-Key is still in flight. |
429 | Monthly/daily sending quota, test-send allowance, or account request rate exceeded. |
What is a tag?
A tag is a short label you attach to a sent message — think hashtag, not category. Tags are how you slice your sending data later: every event, dashboard chart, suppression filter, and webhook payload carries the tags of the originating message, so you can ask questions like "what's the bounce rate of password-reset emails this week?" without scanning the body or subject.
| Parameter | Type | Required | Description |
|---|---|---|---|
What they look like | string | Optional | Lowercase, alphanumeric + - _ :, max 128 chars. Examples: welcome, order-receipt, locale:en, variant:b. |
Where you set them | placement | Optional | On a single send, on a batch (applies to all), or per-recipient inside recipients[].tags. |
Where they show up | surfaces | Optional | Activity log filters, the Metrics dashboard, every webhook event under tags[], and the GET /v3/events?tag=… query. |
Tags vs. metadata | when to use which | Optional | Use tags for things you'll group or filter by ("did this campaign perform?"). Use metadata for IDs you'll look up later ("show me the message for order #482910"). Tags are indexed and faceted; metadata is opaque. |
Limits | quotas | Optional | Max 10 tags per message. The first 1,000 distinct tags per account become facets in the dashboard; beyond that they're still searchable, just not pre-aggregated. |
Code examples
curl -X POST https://api.wemail.io/v3/messages.batch \
-H "Authorization: Bearer afn_live_…" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <noreply@updates.acme.io>",
"subject": "Hi {{first_name}}, your {{month}} statement is ready",
"html": "<p>Hi {{first_name}}, your balance is {{balance}}.</p>",
"global_vars": { "month": "May", "portal_url": "https://acme.io/billing" },
"tags": ["statement", "may-2026"],
"headers": { "X-TES-MSGID": "tes-batch-may26" },
"recipients": [
{ "to": "alex@example.com", "vars": { "first_name": "Alex", "balance": "$42.10" } },
{ "to": "ben@example.com", "vars": { "first_name": "Ben", "balance": "$11.00" } }
]
}'await wemail.messages.batch({
from: "Acme <noreply@updates.acme.io>",
subject: "Hi {{first_name}}, your {{month}} statement is ready",
html: "<p>Hi {{first_name}}, your balance is {{balance}}.</p>",
global_vars: { month: "May", portal_url: "https://acme.io/billing" },
tags: ["statement", "may-2026"],
// applied to every message in the batch; echoed on each message's webhooks
headers: { "X-TES-MSGID": "tes-batch-may26" },
recipients: users.map(u => ({
to: u.email,
name: u.fullName,
vars: { first_name: u.firstName, balance: u.balance },
tags: [`plan:${u.plan}`, `locale:${u.locale}`],
metadata: { user_id: u.id, variant: u.experimentVariant },
})),
});wemail.messages.batch(
from_="Acme <noreply@updates.acme.io>",
subject="Hi {{first_name}}, your {{month}} statement is ready",
html="<p>Hi {{first_name}}, your balance is {{balance}}.</p>",
global_vars={"month": "May", "portal_url": "https://acme.io/billing"},
tags=["statement", "may-2026"],
recipients=[
{ "to": u.email, "vars": {"first_name": u.first_name, "balance": u.balance} }
for u in users
],
)$wemail->messages->batch([
'from' => 'noreply@updates.acme.io',
'subject' => 'Hi {{first_name}}',
'html' => '<p>Your balance is {{balance}}</p>',
'recipients' => $recipients,
]);wemail.messages.batch(
from: "noreply@updates.acme.io",
subject: "Hi {{first_name}}",
html: "<p>Your balance is {{balance}}</p>",
recipients: recipients
)_, err := client.Messages.Batch(ctx, &wemail.BatchParams{
From: "noreply@updates.acme.io",
Subject: "Hi {{first_name}}",
HTML: "<p>Your balance is {{balance}}</p>",
Recipients: recipients,
})wemail.messages().batch(BatchParams.builder()
.from("noreply@updates.acme.io")
.subject("Hi {{first_name}}")
.html("<p>Your balance is {{balance}}</p>")
.recipients(recipients).build());await wemail.Messages.BatchAsync(new BatchParams {
From = "noreply@updates.acme.io",
Subject = "Hi {{first_name}}",
Html = "<p>Your balance is {{balance}}</p>",
Recipients = recipients,
});{
"batch_id": "batch_01HBZK22N4DQ",
"accepted": 2,
"rejected": 0,
"message_ids": [
"msg_01HBZK22NB3Q…",
"msg_01HBZK22NB4R…"
]
}