Emails
Send a message, schedule one, send up to a hundred at once, or read the message log.
Send an email
Returns 202. The message is queued and delivered by a background worker, so this response tells you the message was accepted, not that it arrived. Delivery outcome comes back as a webhook.
Body
| Field | Type | Notes |
|---|---|---|
from | string | object | Required. "a@b.com" or { email, name }. Domain must be verified. |
to | string | array | Required. At least one recipient. |
cc, bcc, replyTo | string | array | Optional. bcc never appears in headers. |
subject | string | Required, up to 998 characters. |
html, text | string | At least one required. Send both. |
templateId | string | Template id or name. Cannot be combined with html or text. |
templateData | object | Values substituted into the template. |
headers | object | Custom headers. Reserved names are rejected. |
attachments | array | { filename, content, contentType, disposition, contentId }. content is base64. |
tags | object | Up to 10 labels, echoed on every event for this message. |
scheduledAt | string | ISO 8601. Up to 30 days ahead. |
Reserved headers
From, To, Subject, Message-ID, Return-Path, DKIM-Signature and similar are rejected rather than silently dropped. Overriding the envelope is how one tenant would forge another’s mail.
Order of checks
Requests are validated in a fixed order, so the error you get is the first real problem rather than an arbitrary one: provisioning, then size, then domain ownership, then verification, then suppression, then the daily and monthly limits. Limits are last so a rejected message never consumes it.
Attachments
{
"attachments": [
{
"filename": "invoice.pdf",
"content": "JVBERi0xLjQKJcfs…",
"contentType": "application/pdf"
},
{
"filename": "logo.png",
"content": "iVBORw0KGgo…",
"contentType": "image/png",
"disposition": "inline",
"contentId": "logo"
}
]
}Reference an inline attachment from your HTML as <img src="cid:logo">. Inline attachments require a contentId.
Send a batch
Takes an array of up to 100 message objects. Each one goes through exactly the same checks as a single send — a batch is not a way around suppression or your limits.
One invalid message does not reject the rest. If any fail, the response is 207 with per-message results; if all succeed it is 202.
{
"accepted": 1,
"rejected": 1,
"results": [
{ "index": 0, "status": "accepted", "id": "msg_use1_01JQ…" },
{
"index": 1,
"status": "rejected",
"error": { "code": "recipient_suppressed" }
}
]
}Retrieve a message
{
"id": "msg_use1_01JQ…",
"from": "hello@yourdomain.com",
"to": ["user@example.com"],
"subject": "Your order shipped",
"status": "delivered",
"error": null,
"sentAt": "2026-07-26T10:31:02.000Z"
}Status moves through queued or scheduled, then sent, then a terminal delivered, bounced, complained or failed. Terminal states are never overwritten by a later non-terminal event, because per-recipient events can arrive out of order.
List messages
Query with limit (max 100) and before, a cursor taken from nextCursor. Newest first.