Batch Transactional Email API (2026): Up to 100 Personalized Messages with Rovela
Use Rovela POST /emails/batch for up to 100 personalized transactional messages: validation, suppression, Idempotency-Key per chunk, and when to use broadcasts instead.
A batch transactional email API (not a marketing blast)
A batch transactional email API sends many personalized messages in one HTTP request — trial reminders, renewal notices, per-user digests — without turning them into a newsletter. Rovela’s endpoint is POST https://api.rovela.dev/emails/batch: up to 100 ordinary send bodies, each with its own recipient and content.
This is not Bulk Email Sending or Bulk Email vs Single Send (broader bulk vs single), and it is not a broadcast. For marketing list fan-out, one-click unsubscribe, and topic suppression, use broadcasts (Transactional vs Marketing Email). The field-level reference is the Send Email API; the behavior notes below follow the batch and scheduled example.
Start from the Email API product page, the Quick Start, the Send Email API, and Authentication.
Related product guides in this series:
- Transactional Email API
- HTTP Email API
- Migrate SMTP to an Email API
- Email API for SaaS Apps
- Send Email with curl
- Transactional Email API + Hosted Mailboxes
- Inbound and Outbound Email API
- Email Sending API
- Email Notification API
- Email API Integration
- Transactional Email Webhooks
- A Simple Transactional Email API
- Send Transactional Email from Your App
- Verified Domain Email API
The request
curl -X POST https://api.rovela.dev/emails/batch \
-H "Authorization: Bearer re_your_sending_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reminders-2026-11-10" \
-d '[
{
"from": "Acme <reminders@mail.acme.com>",
"to": "alice@example.com",
"subject": "Your trial ends in 3 days",
"html": "<p>Hi Alice, your trial ends on Nov 13.</p>",
"text": "Hi Alice, your trial ends on Nov 13."
},
{
"from": "Acme <reminders@mail.acme.com>",
"to": "bob@example.com",
"subject": "Your trial ends in 3 days",
"html": "<p>Hi Bob, your trial ends on Nov 13.</p>",
"text": "Hi Bob, your trial ends on Nov 13."
}
]'
The response is { "object": "list", "data": [ { "object": "email", "id": "…" }, … ] } in the same order as the array you sent. Send an Idempotency-Key for the chunk (for example digest-2026-11-10-0) so a crashed job can safely retry that slice (Idempotency Keys for Email APIs).
Each entry uses the same schema as POST /emails (from, to, subject, html / text, scheduled_at, attachments, tags, …) with two deliberate exceptions: template and topic_id are rejected, not ignored. A batch that silently dropped topic_id could mail people who had unsubscribed from that topic; one that dropped template would send a message with no body. Send those through POST /emails one at a time, or render HTML yourself before batching.
How a batch behaves
From the batch example:
- Validated before anything is written or charged. One bad entry — an unverified domain, an empty body — rejects the whole request with a
400that names the problem. Fix the entry and resubmit the same idempotency key only after you understand whether anything was accepted (a400means nothing was written). - After acceptance, entries deliver independently. A later bounce on one recipient does not retract the others.
- Suppressed addresses are skipped, not refused. Unlike a single send, a batch does not reject an entry for a suppressed recipient. It is accepted, then skipped at delivery with status
suppressedand anemail.suppressedevent. - Billed per recipient, not cheaper than 100 singles. A batch of 100 costs the same as 100 single sends and counts against the rate limit the same way (
429can appear partway through a large job; each message counts individually). Splitting a job into batches makes it faster, not cheaper. scheduled_atworks on each entry, same ISO 8601 rule (include the offset). Suppressions are checked again at delivery time (Scheduled Email Sending).
The 50-recipient cap still applies inside each entry (to+cc+bcc).
Sending more than 100
async function sendAll(messages) {
for (let i = 0; i < messages.length; i += 100) {
const chunk = messages.slice(i, i + 100);
const res = await fetch("https://api.rovela.dev/emails/batch", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ROVELA_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "digest-2026-11-10-" + i,
},
body: JSON.stringify(chunk),
});
if (res.status === 429) {
await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("retry-after") ?? 1)));
i -= 100; // same chunk again
continue;
}
if (!res.ok) throw new Error("chunk " + i + ": " + (await res.json()).message);
}
}
This pattern is taken from the official example. Use a sending_access key on the worker.
When not to use batch
- Sign-up OTPs and password resets — one
POST /emailsper action, with a per-attempt key (Sending OTP Codes by Email, Email API for SaaS Apps). - The same marketing body to a contact list or segment — POST /broadcasts.
- You only have one or two recipients — a single
POST /emailsis simpler.
FAQ
Can I mix scheduled_at and immediate entries in one batch? Each entry is its own message; set scheduled_at only on those that should wait. The example documents scheduled_at on any send, single or batch entry.
How do I cancel one message from a batch? POST /emails/:id/cancel with full_access, while it is still scheduled or queued but not yet handed to the mail server — same as a single send (Send Email with curl).
Does the API reference’s “processed independently” contradict all-or-nothing validation? Read them together: the example is explicit that validation is all-or-nothing before write/charge; after a batch is accepted, delivery outcomes are per message. When in doubt, treat a 400 as “nothing queued” and inspect a 200 list of ids.
Personalize in bulk, keep it transactional
Render each body in your job, POST /emails/batch in chunks of 100, and key each chunk. That is the batch transactional email API — useful for reminders and digests, and intentionally the wrong tool for a campaign.