rootmail

Platform

Webhooks

Signed, idempotent events for the full email lifecycle and inbound replies.

Register an endpoint and rootmail POSTs a signed JSON event whenever something happens to your mail. Deliveries are retried with backoff and each endpoint has a delivery log you can inspect and replay.

Manage endpoints

GET/v1/webhook-endpoints
POST/v1/webhook-endpoints
GET/v1/webhook-endpoints/:id
PATCH/v1/webhook-endpoints/:id
GET/v1/webhook-endpoints/:id/deliveries
DELETE/v1/webhook-endpoints/:id

Events

message.senteventAccepted and handed to the provider.
message.deliveredeventConfirmed delivered to the recipient's server.
message.openedeventThe recipient opened it.
message.clickedeventThe recipient clicked a tracked link.
message.bouncedeventHard or soft bounce — the reason is included.
message.complainedeventMarked as spam by the recipient.
message.failedeventCould not be sent.
message.suppressedeventSkipped because the address was on the suppression list.
message.receivedeventAn inbound reply arrived (threaded).

Verify the signature

Every delivery carries a Rootmail-Signature header: a timestamp and an HMAC-SHA256 of `${timestamp}.${rawBody}` keyed with your endpoint's signing secret. Verify it before trusting a payload, and reject stale timestamps to stop replays.

verify-webhook.ts
import crypto from "node:crypto";

// header: "Rootmail-Signature: t=1718531200,v1=<hex>"
export function verify(raw: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${raw}`)
    .digest("hex");
  const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5 min
  return ok && fresh;
}
CarefulVerify against the RAW request body, before any JSON parsing or middleware reformats it — re-serialized JSON won't match the signature.
noteAn endpoint that fails 10 deliveries in a row is auto-disabled; re-enable it from the dashboard once your receiver is healthy.