Skip to main content
MEGA can POST a signed event to your endpoint whenever a new lead is created. Managing webhooks requires the public_api:webhooks:manage scope.

Register a webhook

POST https://app.gomega.ai/api/agents/crm/lead-webhooks
curl -X POST "https://app.gomega.ai/api/agents/crm/lead-webhooks" \
  -H "Authorization: Bearer $MEGA_TOKEN" \
  -H "x-customer-id: $MEGA_CUSTOMER_ID" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://api.your-company.com/hooks/mega-leads", "event_types": ["lead.created"] }'
Response (201)
{
  "webhook": {
    "id": "wh_...",
    "url": "https://api.your-company.com/hooks/mega-leads",
    "event_types": ["lead.created"],
    "is_active": true,
    "timeout_seconds": 10,
    "retry_attempts": 5,
    "description": null,
    "created_at": "2026-07-02T14:00:00.000Z",
    "updated_at": "2026-07-02T14:00:00.000Z"
  },
  "secret": "mega_whsec_9f86d081...."
}
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}.

Event payload

Each delivery is a JSON POST with this body:
{
  "event": "lead.created",
  "delivered_at": "2026-07-02T14:05:00.000Z",
  "lead": {
    "id": "3f8c...",
    "contact_name": "Jane Buyer",
    "contact_phone": "+12065550123",
    "contact_email": "[email protected]",
    "source_platform": "referral",
    "source_type": "form",
    "lead_line": "buyer",
    "current_stage": { "slug": "new", "name": "New" },
    "custom_fields": { "budget": "500000" },
    "created_at": "2026-07-02T14:04:59.000Z",
    "updated_at": "2026-07-02T14:04:59.000Z"
  }
}

Delivery headers

HeaderMeaning
X-Mega-Signaturesha256=<hex> — HMAC-SHA256 over `${timestamp}.${rawBody}` using your webhook secret.
X-Mega-TimestampUnix seconds; folded into the signature.
X-Mega-DeliveryDelivery UUID — use it to de-duplicate retries.

Verify the signature

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");
  }
);

Replay protection

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.

Retries & delivery semantics

  • 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.