The secret is returned exactly once, at creation (and again only when you rotate_secret). Store it securely — it’s the key you verify signatures with. The URL must be public HTTPS; internal/loopback URLs are rejected (SSRF protection).
Manage webhooks with GET /api/agents/crm/lead-webhooks (list; secrets never returned), PATCH /api/agents/crm/lead-webhooks/{id} (update; rotate_secret: true mints a new secret), and DELETE /api/agents/crm/lead-webhooks/{id}.
Compute HMAC_SHA256(secret, timestamp + "." + rawRequestBody) and compare (constant-time) to the hex in X-Mega-Signature. Verify against the raw request body bytes — do not re-serialize the parsed JSON.
import crypto from "node:crypto";import express from "express";const app = express();const SECRET = process.env.MEGA_WEBHOOK_SECRET;const TOLERANCE_SECONDS = 5 * 60;// Capture the RAW body — signature is over the exact bytes we received.app.post( "/hooks/mega-leads", express.raw({ type: "application/json" }), (req, res) => { const signature = req.get("X-Mega-Signature") || ""; const timestamp = req.get("X-Mega-Timestamp") || ""; const rawBody = req.body.toString("utf8"); // Replay guard: reject stale timestamps. const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)); if (!timestamp || Number.isNaN(age) || age > TOLERANCE_SECONDS) { return res.status(400).send("stale or missing timestamp"); } const expected = "sha256=" + crypto .createHmac("sha256", SECRET) .update(`${timestamp}.${rawBody}`) .digest("hex"); const ok = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!ok) return res.status(401).send("bad signature"); const event = JSON.parse(rawBody); // TODO: de-dupe on req.get("X-Mega-Delivery"), then process event.lead res.status(200).send("ok"); });
X-Mega-Timestamp is part of the signed material. Reject deliveries whose timestamp is outside a tolerance window (~5 minutes is recommended, as shown above) so a captured payload can’t be replayed later.
Respond with a 2xx to acknowledge. Any non-2xx (or a timeout) is retried.
Default timeout is 10s and MEGA makes up to retry_attempts + 1 attempts (default retry_attempts: 5, i.e. up to 6 total), with exponential backoff capped at 30s. Both are configurable per webhook (timeout_seconds 1–60, retry_attempts 0–10).
Redirects are not followed and SSRF-blocked URLs are terminal — neither is retried.
Delivery is at-least-once: the same event may arrive more than once. De-duplicate on X-Mega-Delivery.
Only lead.created is emitted today, and only on a genuine new-lead insert (not on merges into an existing lead). Bulk-uploaded leads do not fire the webhook.