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

ParameterTypeRequiredDescription
fromstringRequiredVerified sender. Format: "Name <email@domain.com>" or just the address.
from_namestringOptionalDisplay name for the From header, applied to every message in the batch. Overrides a name embedded in from.
subjectstringRequiredSubject line. Supports merge tags from recipients[].vars and global_vars.
htmlstringOptionalHTML body with merge tags.
textstringOptionalPlain-text alternative.
templatestringOptionalTemplate ID — used in place of html/text.
recipientsarray of objectsRequiredUp to 5,000 entries. See Recipient object below.
global_varsobjectOptionalMerge values shared by every recipient (e.g. campaign-wide URLs, brand name). Per-recipient vars override matching keys.
tagsarray of stringsOptionalTags applied to every message in the batch (max 10). See Tags.
headersobjectOptionalCustom 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_urlstring (URL)OptionalPer-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.
attachmentsarrayOptionalFile 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.

ParameterTypeRequiredDescription
tostring or array of stringsRequiredThe 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.
namestringOptionalDisplay name for this recipient. If set, the To header becomes "Name <email>".
varsobjectOptionalPer-recipient merge variables, merged over global_vars (recipient wins on matching keys). Keys become available as {{key}} in the subject, HTML, and text.
tagsarray of stringsOptionalExtra 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").
metadataobjectOptionalFree-form key/value pairs stored with this recipient’s message — your internal IDs, A/B variant, etc. Not visible to the recipient.
send_atISO 8601 timestampOptionalSchedule 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

StatusDescription
201Created. { 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.
400Structural 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.
403Sandbox send to a recipient not on the confirmed test allow-list, or the account is suspended.
404The template id doesn't exist on your account.
409A request with the same Idempotency-Key is still in flight.
429Monthly/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.

ParameterTypeRequiredDescription
What they look likestringOptionalLowercase, alphanumeric + - _ :, max 128 chars. Examples: welcome, order-receipt, locale:en, variant:b.
Where you set themplacementOptionalOn a single send, on a batch (applies to all), or per-recipient inside recipients[].tags.
Where they show upsurfacesOptionalActivity log filters, the Metrics dashboard, every webhook event under tags[], and the GET /v3/events?tag=… query.
Tags vs. metadatawhen to use whichOptionalUse 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.
LimitsquotasOptionalMax 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
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" } }
    ]
  }'
Node.js
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 },
  })),
});
Python
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
    ],
)
PHP
$wemail->messages->batch([
  'from' => 'noreply@updates.acme.io',
  'subject' => 'Hi {{first_name}}',
  'html' => '<p>Your balance is {{balance}}</p>',
  'recipients' => $recipients,
]);
Ruby
wemail.messages.batch(
  from: "noreply@updates.acme.io",
  subject: "Hi {{first_name}}",
  html: "<p>Your balance is {{balance}}</p>",
  recipients: recipients
)
Go
_, 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,
})
Java
wemail.messages().batch(BatchParams.builder()
  .from("noreply@updates.acme.io")
  .subject("Hi {{first_name}}")
  .html("<p>Your balance is {{balance}}</p>")
  .recipients(recipients).build());
.NET
await wemail.Messages.BatchAsync(new BatchParams {
  From = "noreply@updates.acme.io",
  Subject = "Hi {{first_name}}",
  Html = "<p>Your balance is {{balance}}</p>",
  Recipients = recipients,
});
ResponseExample response
{
  "batch_id": "batch_01HBZK22N4DQ",
  "accepted": 2,
  "rejected": 0,
  "message_ids": [
    "msg_01HBZK22NB3Q…",
    "msg_01HBZK22NB4R…"
  ]
}