Sending
Messages
Send email — inline or templated — and read its full lifecycle back.
A message is a single email to a single recipient. Send inline HTML, or reference a template by slug or id and pass variables.
Send a message
POST
/v1/messagestorequiredstring | Recipient address, or { email, name }. |
subjectstring | Required unless the template supplies one. |
html / textstring | Inline body. Omit when using a template. |
templatestring | A template slug. Use template_id for the id. |
variablesobject | Values merged into the template (Handlebars). A saved contact's details merge in automatically first — see Personalization below. |
from / reply_tostring | Override the sender / reply-to (must be a verified sender). |
typeenum | `transactional` (default) or `marketing` — meters against the right wing. |
send_atstring | ISO 8601 — schedule for later instead of sending now. |
idempotency_keystring | Exactly-once — see Idempotency. |
tags / metadataarray / object | Your own labels; echoed back on events. |
send-template.ts
const msg = await mail.messages.create({
to: { email: "ada@example.com", name: "Ada" },
template: "welcome",
variables: { name: "Ada", action_url: "https://acme.com/start" },
tags: ["onboarding"],
idempotencyKey: `welcome-${user.id}`,
});
// → { id: "msg_…", status: "queued", … }Raw request & response
Under the SDK it's a plain JSON POST. The body is snake_case; the response is the full message object (also returned by GET /v1/messages/:id).
POST /v1/messages — request body
{
"to": { "email": "ada@example.com", "name": "Ada" },
"type": "transactional",
"template": "welcome",
"variables": { "name": "Ada", "action_url": "https://acme.com/start" },
"tags": ["onboarding"],
"idempotency_key": "welcome-42"
}201 Created — the message
{
"id": "msg_1a2b3c4d5e",
"object": "message",
"type": "transactional",
"status": "queued",
"to": "ada@example.com",
"from": { "email": "hello@acme.com", "name": "Acme" },
"reply_to": null,
"subject": "Welcome to Acme",
"sub_tenant_id": null,
"template_id": "tpl_welcome",
"template_version": 3,
"priority": "normal",
"tags": ["onboarding"],
"metadata": {},
"attachments": [],
"opened_at": null,
"clicked_at": null,
"idempotency_key": "welcome-42",
"provider": null,
"provider_message_id": null,
"content_hash": "b1946ac9…",
"sandbox": false,
"error": null,
"scheduled_at": null,
"created_at": "2026-07-22T10:00:00Z",
"updated_at": "2026-07-22T10:00:00Z"
}notePass idempotency either as the
idempotency_key field or an Idempotency-Key request header — a retried key returns the SAME message instead of sending twice.Personalization
When the recipient is a saved contact, their details fill the template's {{placeholders}} automatically — you don't have to look anything up or pass it per send. The same happens for every campaign recipient and sequence enrollee.
{{email}},{{name}},{{first_name}},{{last_name}},{{phone}}— from the contact record (first/last are split from the name).- Every custom field on the contact's
metadatais available by its own name — import once, personalize everywhere. - Precedence: your explicit
variablesoverride contact fields, which override the derived built-ins.{{unsubscribe_url}}is always rootmail's signed link.
personalization.ts
// Contact ada@example.com has { name: "Ada Lovelace", metadata: { plan: "Growth" } }
await mail.messages.create({
to: "ada@example.com",
template: "renewal", // "Hi {{first_name}}, your {{plan}} plan renews soon."
// → "Hi Ada, your Growth plan renews soon." — no variables needed.
});Retrieve, audit & prove
GET
/v1/messagesGET
/v1/messages/:idGET
/v1/messages/:id/auditGET
/v1/messages/:id/proofaudit.ts
const { trail } = await mail.messages.audit(msg.id);
// queued → sending → sent → delivered → opened …
const proof = await mail.messages.proof(msg.id); // { bundle, signature }GET /v1/messages/:id/audit — response
{
"message_id": "msg_1a2b3c4d5e",
"status": "delivered",
"trail": [
{ "event": "queued", "actor": "api", "timestamp": "2026-07-22T10:00:00Z", "metadata": {} },
{ "event": "sending", "actor": "system", "timestamp": "2026-07-22T10:00:01Z", "metadata": {} },
{ "event": "sent", "actor": "system", "provider": "ses", "provider_message_id": "0100018f…", "timestamp": "2026-07-22T10:00:02Z", "metadata": {} },
{ "event": "delivered", "actor": "system", "timestamp": "2026-07-22T10:00:05Z", "metadata": {} },
{ "event": "opened", "actor": "system", "ip": "9x.x.x.x", "user_agent": "Mozilla/5.0…", "timestamp": "2026-07-22T10:14:00Z", "metadata": {} }
]
}noteSuppressed, bounced, and unsubscribed recipients are checked automatically before every send — you can't accidentally email someone who opted out.