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-endpointsPOST
/v1/webhook-endpointsGET
/v1/webhook-endpoints/:idPATCH
/v1/webhook-endpoints/:idGET
/v1/webhook-endpoints/:id/deliveriesDELETE
/v1/webhook-endpoints/:idEvents
message.sentevent | Accepted and handed to the provider. |
message.deliveredevent | Confirmed delivered to the recipient's server. |
message.openedevent | The recipient opened it. |
message.clickedevent | The recipient clicked a tracked link. |
message.bouncedevent | Hard or soft bounce — the reason is included. |
message.complainedevent | Marked as spam by the recipient. |
message.failedevent | Could not be sent. |
message.suppressedevent | Skipped because the address was on the suppression list. |
message.receivedevent | An 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.