rootmail

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/messages
torequiredstringRecipient address, or { email, name }.
subjectstringRequired unless the template supplies one.
html / textstringInline body. Omit when using a template.
templatestringA template slug. Use template_id for the id.
variablesobjectValues merged into the template (Handlebars). A saved contact's details merge in automatically first — see Personalization below.
from / reply_tostringOverride the sender / reply-to (must be a verified sender).
typeenum`transactional` (default) or `marketing` — meters against the right wing.
send_atstringISO 8601 — schedule for later instead of sending now.
idempotency_keystringExactly-once — see Idempotency.
tags / metadataarray / objectYour 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 metadata is available by its own name — import once, personalize everywhere.
  • Precedence: your explicit variables override 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/messages
GET/v1/messages/:id
GET/v1/messages/:id/audit
GET/v1/messages/:id/proof
audit.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.
Messages · rootmail developers