> ## Documentation Index
> Fetch the complete documentation index at: https://dev.gomega.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive lead.created events, verify the HMAC signature, and handle retries.

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
```

```bash theme={null}
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"] }'
```

```json Response (201) theme={null}
{
  "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...."
}
```

<Warning>
  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).
</Warning>

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:

```json theme={null}
{
  "event": "lead.created",
  "delivered_at": "2026-07-02T14:05:00.000Z",
  "lead": {
    "id": "3f8c...",
    "contact_name": "Jane Buyer",
    "contact_phone": "+12065550123",
    "contact_email": "jane@example.com",
    "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

| Header             | Meaning                                                                                      |
| ------------------ | -------------------------------------------------------------------------------------------- |
| `X-Mega-Signature` | `sha256=<hex>` — HMAC-SHA256 over `` `${timestamp}.${rawBody}` `` using your webhook secret. |
| `X-Mega-Timestamp` | Unix seconds; folded into the signature.                                                     |
| `X-Mega-Delivery`  | Delivery 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.

<CodeGroup>
  ```js Node.js (Express) theme={null}
  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");
    }
  );
  ```

  ```python Python (Flask) theme={null}
  import hashlib, hmac, time
  from flask import Flask, request, abort

  app = Flask(__name__)
  SECRET = b"mega_whsec_...."   # from your secret manager
  TOLERANCE_SECONDS = 5 * 60

  @app.post("/hooks/mega-leads")
  def mega_leads():
      signature = request.headers.get("X-Mega-Signature", "")
      timestamp = request.headers.get("X-Mega-Timestamp", "")
      raw_body = request.get_data()  # exact bytes

      try:
          age = abs(int(time.time()) - int(timestamp))
      except ValueError:
          abort(400)
      if age > TOLERANCE_SECONDS:
          abort(400)  # replay guard

      expected = "sha256=" + hmac.new(
          SECRET, f"{timestamp}.".encode() + raw_body, hashlib.sha256
      ).hexdigest()

      if not hmac.compare_digest(signature, expected):
          abort(401)

      event = request.get_json()
      # TODO: de-dupe on request.headers["X-Mega-Delivery"], then process event["lead"]
      return "ok", 200
  ```
</CodeGroup>

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