Skip to content

Emails

Send a message, schedule one, send up to a hundred at once, or read the message log.

Send an email

POST/v1/emails

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

FieldTypeNotes
fromstring | objectRequired. "a@b.com" or { email, name }. Domain must be verified.
tostring | arrayRequired. At least one recipient.
cc, bcc, replyTostring | arrayOptional. bcc never appears in headers.
subjectstringRequired, up to 998 characters.
html, textstringAt least one required. Send both.
templateIdstringTemplate id or name. Cannot be combined with html or text.
templateDataobjectValues substituted into the template.
headersobjectCustom headers. Reserved names are rejected.
attachmentsarray{ filename, content, contentType, disposition, contentId }. content is base64.
tagsobjectUp to 10 labels, echoed on every event for this message.
scheduledAtstringISO 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

json
{
  "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

POST/v1/emails/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.

207 Multi-Status · json
{
  "accepted": 1,
  "rejected": 1,
  "results": [
    { "index": 0, "status": "accepted", "id": "msg_use1_01JQ…" },
    {
      "index": 1,
      "status": "rejected",
      "error": { "code": "recipient_suppressed" }
    }
  ]
}

Retrieve a message

GET/v1/emails/{id}
json
{
  "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

GET/v1/emails

Query with limit (max 100) and before, a cursor taken from nextCursor. Newest first.