Send Email

Queue a transactional message, singly or in a batch.

POST/emailsAPI key or session

Request body

FieldTypeRequiredNotes
fromstringyesSender, as "Acme <hello@example.com>". Must be on a verified domain.
tostring | string[]yesOne or more recipients.
subjectstringyes*Not required when using a template that supplies one.
htmlstringno*HTML body.
textstringno*Plain-text body. Send both where you can.
templateobjectno*{ id, variables }. Mutually exclusive with html and text.
cc / bccstring | string[]noCount as recipients for billing and for the 50 cap.
reply_tostring | string[]noWhere replies should go.
scheduled_atstringnoISO 8601 timestamp in the future.
tagsobject | arraynoArbitrary labels, returned with the message.
headersobjectnoCustom headers.
topic_idstringnoOnly send to contacts subscribed to this topic.
attachmentsarrayno{ filename, content (base64), content_type }.

*At least one of html, text or template is required.

Example

curl -X POST https://api.rovela.dev/emails \
  -H "Authorization: Bearer re_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@yourdomain.com>",
    "to": ["alice@example.com", "bob@example.com"],
    "subject": "Your invoice is ready",
    "html": "<p>Hello {{name}}</p>"
  }'
{"object": "email", "id": "9f1c2e40-..."}

Example: an email verification code

The message almost every product sends first. Generate the code on your server, keep only its hash, and send both an HTML and a plain-text body — many mail clients show the text part in the notification preview, which is where people read a code.

curl -X POST https://api.rovela.dev/emails \
  -H "Authorization: Bearer re_your_sending_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: verify-signup-8f3a2c" \
  -d '{
    "from": "Acme <no-reply@yourdomain.com>",
    "to": "alice@example.com",
    "subject": "Your Acme code: 482913",
    "html": "<p>Your verification code is</p><p style=\"font-size:28px;letter-spacing:6px\"><b>482913</b></p><p>It expires in 10 minutes.</p>",
    "text": "Your verification code is 482913. It expires in 10 minutes."
  }'

The Idempotency-Key makes a retried request safe: if your server times out and sends again, the reader gets one code, not two that disagree. The full recipe — generating the code, storing it, checking it and limiting resends, in Node and Python — is in Email verification codes.

Recipients and billing

A message may address at most 50 recipients across to, cc and bcc combined. That is the transport's limit, not a plan limit, and it is checked before the request is accepted — so nothing is charged for a message the mail server would have refused.

Quota is charged per recipient, not per message. A message to three people costs three of your monthly allowance, and cc and bcc count exactly the same as to — every address is a separate delivery. A batch of 100 messages to two recipients each costs 200.

Idempotency

Send an Idempotency-Key header (up to 256 characters) and a retry of the same request within 24 hours returns the original response instead of sending again. Use it for anything driven by a queue or a webhook, where "did that already happen?" is a question you will otherwise have to answer yourself.

Scheduling

A future scheduled_at queues the message and returns immediately; the id you get back is the message's, so you can read or cancel it before it goes out. POST /emails/:id/cancel cancels a message that has not been sent yet. Reading, rescheduling and cancelling need a full_access key or a dashboard session; a sending_access key can only send.

Batch

POST/emails/batchAPI key or session

Up to 100 messages in one request, each an object with the same schema. Every entry is processed independently, so one bad message does not discard the others, and the response lists an id per entry in order.

Two fields are rejected rather than ignored: template and topic_id. Both refusals are deliberate — a batch that silently dropped topic_id would mail people who had unsubscribed from that topic, and one that dropped template would send a message with no body. Send those through POST /emails.

curl -X POST https://api.rovela.dev/emails/batch \
  -H "Authorization: Bearer re_your_api_key" \
  -H "Content-Type: application/json" \
  -d '[
    {"from": "hello@yourdomain.com", "to": "a@example.com", "subject": "Hi A", "text": "Hello A"},
    {"from": "hello@yourdomain.com", "to": "b@example.com", "subject": "Hi B", "text": "Hello B"}
  ]'
{
  "object": "list",
  "data": [
    {"object": "email", "id": "<uuid1>"},
    {"object": "email", "id": "<uuid2>"}
  ]
}

Reading a message

GET/emails/:idfull_access key or session

The current state of a message. A 200 from the send endpoint means accepted for delivery, not delivered — this is where you find out what happened after that.

Events

GET/emails/:id/eventsfull_access key or session

The timeline for one message: accepted, delivered, opened, clicked, bounced or failed, with a timestamp on each. Useful for debugging a single message; for anything systematic, subscribe to a webhook instead of polling this.

Errors

  • 400 — a missing required field, a base64 attachment that does not decode, a from domain not belonging to your organization, a template that does not exist, or more than 50 recipients on one message.
  • 401 — missing or invalid credential, or a sending_access key used to read, reschedule or cancel a message.
  • 429 — rate limit exceeded. Each message in a batch counts individually, so a large batch can trip this partway through.