Closed betaInvite-only API access.Ask for an invite

One call to send. One honest word back.

Send email, run campaigns, and receive replies through one API. Try a sandbox request below, then follow delivery events on your webhook.

send.ts
import { RootMail } from "@rootmail/node";

const mail = new RootMail({ apiKey: process.env.ROOTMAIL_API_KEY! });

const msg = await mail.send({
  to: "delivered@test.rootmail.dev",
  subject: "Your booking is confirmed",
  html: "<p>See you Friday.</p>",
  idempotencyKey: "demo-8f21c4",
});
sandbox · real API · rate limited · recipients forced to the mailbox simulator
202 Accepted · 218mscached example
{
  "id": "msg_8haaujzmx7g3pv5kjlsoon5e",
  "object": "message",
  "status": "queued",
  "to": "delivered@test.rootmail.dev",
  "subject": "Your booking is confirmed",
  "sandbox": true
}

queued is the only status we can honestly return in a request. Everything after it arrives on your webhook, and we draw the difference.

We record what we did, not only what happened.

One append-only trail. Your customers' throttles and DNS drift arrive on the same webhook as your deliveries.

  1. 11:20:55dkim_rotation_startedsunsetvillas.com · selector rootmail-202608 published
  2. 11:02:18tenant_dns_driftednorthlakegym.com · DKIM stopped resolving · 6h grace
  3. 10:31:09tenant_throttledharbourclinic.com · 60/hour · complaints 0.31% · throttle at 0.30%
  4. 10:12:44suppressedmsg_iq3q… · added to the suppression list
  5. 10:12:44bouncedmsg_iq3q… · 550 5.1.1 mailbox unavailable
  6. 10:01:30tenant_warnedharbourclinic.com · complaints 0.12% · warn at 0.10%
  7. 09:42:10clickedmsg_8haa… · redirect recorded
  8. 09:41:55openedmsg_8haa… · tracking pixel · undercounts blocked images
  9. 09:14:07deliveredmsg_8haa… · guest@test.rootmail.dev
  10. 09:14:03sentmsg_8haa… · provider accepted
  11. 09:14:02sendingmsg_8haa… · provider ses
  12. 09:14:02queuedmsg_8haa… · api_key

The hollow node is an open — a pixel fired, and mail clients prefetch images.

Rootmail-Signature · HMAC-SHA256 of timestamp + raw body

The same key twice sends once.

One key. Two requests. One message — and a response header that names the replay.

Idempotency-Keydemo-8f21c4
request 1 202 Accepted · 218mscached example
{
  "id": "msg_8haaujzmx7g3pv5kjlsoon5e",
  "status": "queued"
}
request 2 200 OK · 31mscached example
Idempotent-Replayed: true
{
  "id": "msg_8haaujzmx7g3pv5kjlsoon5e",
  "status": "queued"
}
one messagemsg_8haaujzmx7g3pv5kjlsoon5e

sandbox · real API · rate limited · recipients forced to the mailbox simulator

One integration. A branch for every customer you send for.

Their domain, their DKIM key, their reputation — hanging off the one API key you already integrated.

sub_tenantsdemonstration

your platform

harbourbookings.com

one API key · one webhook · one integration

0.9%bounces7d · provider feedback · warn at 5.00%
0.04%complaints7d · provider feedback · throttle at 0.30%
their domain

01you call· one request

onboard.ts
const tenant = await mail.subTenants.create({
  name: "Sunset Villas",
  sendingDomain: "sunsetvillas.com",
  externalId: "customer_8821",
});

// tenant.dns_records → the table beside this one.
await mail.subTenants.verify(tenant.id);

02they publish· paste into your own onboarding UI

TXT_rootmail.sunsetvillas.comrequired
rootmail-verify=9f3c…
TXTrootmail._domainkey.sunsetvillas.comrequired
v=DKIM1; k=rsa; p=MIIBIjANBg…
TXTsunsetvillas.comoptional
v=spf1 include:spf.rootmail.io ~all
TXT_dmarc.sunsetvillas.comoptional
v=DMARC1; p=none; rua=mailto:dmarc@sunsetvillas.com

03and then they are their own sender· sunsetvillas.com · its own key, its own score

From
bookings@sunsetvillas.com
DKIM
rootmail._domainkey.sunsetvillas.com
0.6%bounces7d · provider feedback · warn at 5.00%
0.02%complaints7d · provider feedback · throttle at 0.30%
Sending
sending · within limits

these two clients are invented · the record shapes, thresholds and throttle rate are the real ones

Every branch is scored on its own trailing 7 days. Cross a line — Cliffside is over the complaint threshold — and a 15-minute sweep throttles that branch, sixty sends an hour, re-queued rather than dropped, while the rest of your platform keeps sending at full rate.

Sign it, verify it, then break it.

proof.ts
const proof = await mail.messages.proof(msg.id);

// Anyone can check it — no key, no account.
await fetch("https://api.rootmail.io/v1/proof/verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(proof), // { bundle, signature }
});
POST /v1/proof/verifyno key required
{
  "object": "proof_verification",
  "valid": true
}
ed25519 · signature verifies · content_hash sha256 included
GET /v1/messages/:id/proof
{
  "message_id": "msg_8haaujzmx7g3pv5kjlsoon5e",
  "content_hash": "7242947b3a9e5bebd1e9f0a7…",
  "subject": "Your booking is confirmed",
  "to": "guest@test.rootmail.dev",
  "from": "bookings@sunsetvillas.com",
  "status": "delivered",
  "workspace_id": "ws_7q2ktdmzr4x9c1n6vbhsyaef",
  "created_at": "2026-08-26T09:14:02.184Z",
  "audit": [
    { "event": "queued",
      "occurred_at": "2026-08-26T09:14:02.184Z", "actor": "api_key" },
    { "event": "sent",
      "occurred_at": "2026-08-26T09:14:03.902Z", "actor": "worker" },
    { "event": "delivered",
      "occurred_at": "2026-08-26T09:14:07.451Z", "actor": "provider" }
  ],
  "issued_at": "2026-08-26T09:52:41.006Z"
}
signature h6WP3QMuEuC5eeoGT+wBt9… · ed25519

POST /v1/proof/verify takes a bundle and a signature, needs no key, and answers someone who does not trust us.

3,000sends a month, free

500 a day, no card. Sandbox sends never count.

npm i @rootmail/node
Get an API keyRead the docs