verify webhooks

Testing tip: create a throwaway endpoint at freewebhook.app and point a Mailway webhook at it. You'll see every delivery — headers, signature, and payload — arrive in real time, which makes wiring up your verification code far easier than guessing at logs.

Every delivery is signed with the Standard Webhooks scheme — the same one Svix popularized — so existing verification libraries work unchanged. Your endpoint receives three headers:

webhook-id:         <event id>
webhook-timestamp:  <unix seconds>
webhook-signature:  v1,<base64 hmac-sha256>

verifying with a library (recommended)

import { Webhook } from "svix"; // or standardwebhooks

const wh = new Webhook(process.env.MAILWAY_WEBHOOK_SECRET); // whsec_...
const payload = wh.verify(rawBody, {
  "webhook-id": req.headers["webhook-id"],
  "webhook-timestamp": req.headers["webhook-timestamp"],
  "webhook-signature": req.headers["webhook-signature"],
}); // throws if invalid

verifying by hand

const crypto = require("crypto");
const secret = Buffer.from(process.env.MAILWAY_WEBHOOK_SECRET.slice(6), "base64");
const signed = `${id}.${timestamp}.${rawBody}`;
const expected = "v1," + crypto.createHmac("sha256", secret).update(signed).digest("base64");
// timing-safe compare against webhook-signature

payload shape

{
  "type": "email.delivered",
  "created_at": "2026-08-05T12:00:00Z",
  "data": { "email_id": "…", ... }
}

retries & the circuit breaker

Failed deliveries retry on a backoff ladder (5s → 5m → 30m → 2h → 5h → 10h). After 10 consecutive failures the endpoint is auto-disabled; redelivering any attempt from the dashboard re-enables it. Respond with a 2xx within 10 seconds — do heavy work async.