Batch Sending
Send emails to multiple recipients in a single API call
Batch sending is only available with Marketing stream credentials. Transactional credentials return 400 BATCH_NOT_ALLOWED_FOR_TRANSACTIONAL. This prevents misuse of transactional streams for bulk email.
Send a Batch
POST https://api.mail.land/v1/transactional/send-batchcurl -X POST https://api.mail.land/v1/transactional/send-batch \
-H "X-API-Key: ml_pem_Ab3Kx9mN2pQ7rT4wL8yD..." \
-H "Content-Type: application/json" \
-d '{
"from": {
"email": "[email protected]",
"name": "Your Company"
},
"recipients": [
{
"email": "[email protected]",
"name": "Alice",
"variables": { "firstName": "Alice", "plan": "Pro" }
},
{
"email": "[email protected]",
"name": "Bob",
"variables": { "firstName": "Bob", "plan": "Starter" }
}
],
"subject": "Hi {{firstName}}, check out what'\''s new",
"htmlContent": "<h1>Hey {{firstName}}!</h1><p>Your {{plan}} plan just got an upgrade.</p>",
"variables": {
"companyName": "Acme Inc",
"year": "2026"
},
"tags": ["product-update", "february-2026"],
"batchName": "February Product Update"
}'Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
from | object | Yes | Sender address (email and optional name) |
recipients | array | Yes | List of recipients (note: recipients, not to) |
recipients[].email | string | Yes | Recipient email |
recipients[].name | string | No | Recipient display name |
recipients[].variables | object | No | Per-recipient template variables |
subject | string | Conditional | Required unless using templateId |
templateId | string | No | Use a stored template |
htmlContent | string | Conditional | HTML body (required if no templateId) |
textContent | string | No | Plain text fallback |
variables | object | No | Global variables. Applied whether or not templateId is set; on a key overlap, the recipient's own recipients[].variables wins |
replyTo | string | No | Reply-to address |
batchName | string | No | Human-readable name for this batch |
tags | array | No | Tags for analytics and webhook filtering |
customHeaders | object | No | Custom email headers |
tracking | object | No | Open/click tracking overrides |
ipPoolId | string | No | Reserved for dedicated IP pools. Not yet available: sending it returns 400 IP_POOL_NOT_FOUND. Omit it. |
scheduledAt | datetime | No | Schedule for future delivery, up to 7 days ahead |
externalBatchId | string | No | Your system's batch identifier: when supplied, it is echoed back as the response's batchId |
priority | enum | No | Low, Normal (default), High, Critical |
idempotencyKey | string | No | Prevent duplicate sends for the same key. Prefer the X-Idempotency-Key header |
Note the field name difference: batch sending uses recipients while single send uses to.
Response
Status: 202 Accepted
{
"success": true,
"batchId": "batch_f47ac10b58cc43729876fedcba098765",
"messageIds": ["m_a1b2c3...", "m_d4e5f6..."],
"totalRecipients": 2,
"acceptedCount": 2,
"rejectedCount": 0,
"rejectedRecipients": [],
"queuedAt": "2026-02-17T10:30:00Z",
"estimatedCompletion": "2026-02-17T10:35:00Z",
"isSandboxed": false
}| Field | Type | Description |
|---|---|---|
batchId | string | Unique batch identifier |
messageIds | array | Per-recipient message IDs (prefixed m_ for marketing) |
totalRecipients | int | Total recipients in the request |
acceptedCount | int | Recipients accepted for delivery |
rejectedCount | int | Recipients rejected |
rejectedRecipients | array | Details of rejected recipients with reasons |
estimatedCompletion | datetime | Estimated time for batch processing completion |
Variable Merging
Per-recipient variables override global variables when keys overlap:
{
"variables": {
"companyName": "Acme Inc",
"greeting": "Hello"
},
"recipients": [
{
"email": "[email protected]",
"variables": { "greeting": "Dear valued customer" }
},
{
"email": "[email protected]"
}
]
}[email protected]receives:greeting = "Dear valued customer",companyName = "Acme Inc"[email protected]receives:greeting = "Hello",companyName = "Acme Inc"
Error Handling
Batch-specific errors:
| Status | Code | Description |
|---|---|---|
400 | BATCH_NOT_ALLOWED_FOR_TRANSACTIONAL | Transactional credential cannot send batches |
400 | VALIDATION_FAILED | Exceeded 10,000 recipients per batch |
400 | INVALID_REQUEST | scheduledAt more than 7 days ahead ("Cannot schedule email more than 7 days in advance") |
400 | TEMPLATE_NOT_FOUND | templateId names no active template of this account (also returned for a deleted template) |
400 | IP_POOL_NOT_FOUND | ipPoolId names no pool assigned to this account |
422 | TEMPLATE_VARIABLE_MISSING | Every recipient lacks a value for a {{placeholder}} (if only some do, they are skipped and listed in rejectedRecipients with the reason template_variable_missing: name). Nothing is queued |
400 | BATCH_SEND_FAILED | All recipients are suppressed |
429 | RATE_LIMIT_EXCEEDED | Rate limit exceeded (workspace/plan: not per credential) |
429 | QUOTA_EXCEEDED | Monthly quota exhausted |
The account-verification and policy codes documented for POST /v1/transactional/send (SENDING_BLOCKED, ACTION_REQUIRED, DAILY_LIMIT_EXCEEDED, MARKETING_PAUSED, and the rest) apply here too.
Partial success is possible: some recipients may be accepted while others are rejected (e.g., suppressed addresses). Check rejectedRecipients in the response.
Next Steps
- REST API: single email sending reference
- Templates: create templates for batch emails
- Scheduled Emails: schedule batches for later