Batch and scheduled sends
Up to a hundred personalised messages in one request, and mail that goes out later.
A batch: one request, many messages
POST /emails/batch takes an array of up to 100 ordinary send bodies. Each entry is its own message with its own recipient and content — useful for a nightly digest or a round of reminders where every email is slightly different.
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-10-05" \
-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 Oct 8.</p>",
"text": "Hi Alice, your trial ends on Oct 8."
},
{
"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 Oct 8.</p>",
"text": "Hi Bob, your trial ends on Oct 8."
}
]'The response lists one id per entry, in the same order:
{
"object": "list",
"data": [
{"object": "email", "id": "c41a..."},
{"object": "email", "id": "c41b..."}
]
}How a batch behaves
- All or nothing. Every entry is checked before anything is written or charged. One bad entry — an unverified domain, an empty body — rejects the whole batch with a
400that names the problem. - 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. A batch of 100 costs the same as 100 single sends, and counts against the rate limit the same way. Splitting a large job into batches makes it faster, not cheaper.
- No templates. Entries cannot use
templateortopic_id; render the body yourself, or send templated mail throughPOST /emailsone at a time.
Sending a long list
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",
// Stable per chunk, so re-running the job after a crash
// does not send the first chunks twice.
"Idempotency-Key": "digest-2026-10-05-" + 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)
}
}Scheduling: send it later
Add scheduled_at in ISO 8601 to any send — single or batch entry. The message is accepted now and delivered at that time; until then its status is scheduled.
curl -X POST https://api.rovela.dev/emails \
-H "Authorization: Bearer re_your_sending_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@mail.acme.com>",
"to": "alice@example.com",
"subject": "How is your first week going?",
"html": "<p>Hi Alice, any questions so far? Just reply to this email.</p>",
"text": "Hi Alice, any questions so far? Just reply to this email.",
"scheduled_at": "2026-10-12T09:00:00+08:00"
}'Include the offset. A timestamp without one is not a time; it is a different time in every zone it could be read in.
Moving or cancelling it
Both need a full_access key or a dashboard session — the sending_access key that created the message gets 401 here, so changing mail that is already queued takes a deliberate, more privileged step.
# Move it
curl -X PATCH https://api.rovela.dev/emails/<email_id> \
-H "Authorization: Bearer re_your_full_access_key" \
-H "Content-Type: application/json" \
-d '{"scheduled_at": "2026-10-13T09:00:00+08:00"}'
# Cancel it
curl -X POST https://api.rovela.dev/emails/<email_id>/cancel \
-H "Authorization: Bearer re_your_full_access_key"
# {"object": "email", "id": "<email_id>", "cancelled": true}- Rescheduling works only while the message is still
scheduled. Cancelling also works on a message that is queued but not yet handed to the mail server. - Once it is on its way, both return
400— mail that has left cannot be called back. - Suppressions are checked again at delivery time, so an address that bounces between scheduling and sending is skipped rather than mailed.
Every field and error: Emails API.