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

# Webhooks

> Receive real-time event notifications from LeadLex via HTTPS callbacks

Webhooks let your application react to changes in LeadLex the moment they happen. Register an HTTPS endpoint once, and LeadLex will `POST` a signed JSON payload to it whenever a subscribed event fires — no polling required.

<Note>
  Webhooks are production-ready. All payloads are signed with **HMAC-SHA256**, retried with exponential backoff on failure, and protected against SSRF by blocking private IP ranges.
</Note>

## How It Works

1. You register a webhook endpoint by calling `POST /v1/webhooks` with a target URL and a list of events.
2. LeadLex returns a **signing secret** (shown once — store it securely).
3. When a subscribed event occurs, LeadLex sends a `POST` request to your URL with a signed JSON body.
4. Your endpoint must respond with a `2xx` status within **10 seconds**. Otherwise the delivery is retried with exponential backoff.

## Event Types

LeadLex fires 28 distinct event types across all core resources. Subscribe to as many (or as few) as you need.

<CardGroup cols={2}>
  <Card title="Contacts" icon="user">
    `contact.created`<br />
    `contact.updated`<br />
    `contact.deleted`<br />
    `contact.bulk_created`
  </Card>

  <Card title="Companies" icon="building">
    `company.created`<br />
    `company.updated`<br />
    `company.deleted`
  </Card>

  <Card title="Deals" icon="handshake">
    `deal.created`<br />
    `deal.updated`<br />
    `deal.deleted`<br />
    `deal.bulk_created`<br />
    `deal.bulk_updated`
  </Card>

  <Card title="Lists" icon="list">
    `list.created`<br />
    `list.deleted`<br />
    `list.contacts_added`<br />
    `list.contacts_removed`
  </Card>

  <Card title="Campaigns" icon="envelope">
    `campaign.created`<br />
    `campaign.started`<br />
    `campaign.paused`<br />
    `campaign.deleted`<br />
    `campaign.step_created`<br />
    `campaign.contacts_added`
  </Card>

  <Card title="Activities & Tasks" icon="check">
    `activity.created`<br />
    `task.created`<br />
    `task.updated`<br />
    `task.approved`<br />
    `task.dismissed`
  </Card>
</CardGroup>

## Delivery Headers

Every webhook `POST` from LeadLex includes the following headers:

| Header                  | Description                                                                                 |
| ----------------------- | ------------------------------------------------------------------------------------------- |
| `X-LeadLex-Signature`   | `sha256=<hex>` HMAC of the raw request body using your webhook's signing secret.            |
| `X-LeadLex-Timestamp`   | ISO 8601 timestamp of when the delivery was generated. Use to guard against replay attacks. |
| `X-LeadLex-Delivery-ID` | Unique UUID for this delivery attempt. Use for idempotency in your handler.                 |
| `Content-Type`          | Always `application/json`.                                                                  |
| `User-Agent`            | `LeadLex-Webhook/1.0`                                                                       |

## Payload Structure

All webhook payloads share a common envelope:

```json theme={null}
{
  "event": "contact.created",
  "delivery_id": "7f3c4d2a-1b8e-4a9c-9d6f-2e5b8c7a1f3d",
  "occurred_at": "2026-04-17T14:23:05Z",
  "workspace_id": "w_9a8b7c6d5e4f3a2b1c0d",
  "data": {
    "contact": {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "full_name": "Jane Doe",
      "email": "jane@example.com",
      "created_at": "2026-04-17T14:23:05Z"
    }
  }
}
```

The `data` object shape depends on the event type. For `*.bulk_*` events, `data` contains an array plus a `count` field.

## Verifying Signatures

<Warning>
  **Always verify the signature before trusting a webhook.** An attacker who discovers your webhook URL could otherwise forge events. Reject any request where `X-LeadLex-Signature` does not match.
</Warning>

To verify, compute `HMAC-SHA256(secret, raw_body)` and compare it to the hex value in `X-LeadLex-Signature` (after stripping the `sha256=` prefix). Use a **constant-time** comparison to prevent timing attacks.

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib
  from flask import Flask, request, abort

  app = Flask(__name__)
  WEBHOOK_SECRET = "whsec_your_signing_secret_here"

  def verify_signature(raw_body: bytes, signature_header: str) -> bool:
      if not signature_header or not signature_header.startswith("sha256="):
          return False
      received = signature_header.split("=", 1)[1]
      expected = hmac.new(
          WEBHOOK_SECRET.encode("utf-8"),
          raw_body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(received, expected)

  @app.post("/leadlex/webhook")
  def handle_webhook():
      raw = request.get_data()
      sig = request.headers.get("X-LeadLex-Signature", "")

      if not verify_signature(raw, sig):
          abort(401, "invalid signature")

      payload = request.get_json()
      event_type = payload["event"]

      # Dispatch based on event
      if event_type == "contact.created":
          handle_contact_created(payload["data"]["contact"])

      return "", 200
  ```

  ```javascript JavaScript theme={null}
  import express from "express";
  import crypto from "node:crypto";

  const app = express();
  const WEBHOOK_SECRET = "whsec_your_signing_secret_here";

  // Use raw body parser so the signature is computed over the exact bytes
  app.post(
    "/leadlex/webhook",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const signatureHeader = req.get("X-LeadLex-Signature") || "";
      if (!signatureHeader.startsWith("sha256=")) {
        return res.status(401).send("invalid signature");
      }

      const received = signatureHeader.slice("sha256=".length);
      const expected = crypto
        .createHmac("sha256", WEBHOOK_SECRET)
        .update(req.body)
        .digest("hex");

      const ok =
        received.length === expected.length &&
        crypto.timingSafeEqual(
          Buffer.from(received, "hex"),
          Buffer.from(expected, "hex"),
        );

      if (!ok) return res.status(401).send("invalid signature");

      const payload = JSON.parse(req.body.toString("utf8"));

      switch (payload.event) {
        case "contact.created":
          handleContactCreated(payload.data.contact);
          break;
        case "deal.updated":
          handleDealUpdated(payload.data.deal);
          break;
      }

      res.status(200).end();
    },
  );
  ```
</CodeGroup>

<Tip>
  Parse the raw body **before** `JSON.parse` (JS) or `request.get_json()` (Python). Signatures are computed over the exact bytes sent — any re-serialization will break verification.
</Tip>

## Retries & Delivery Guarantees

If your endpoint does not respond with `2xx` within **10 seconds**, LeadLex retries the delivery with exponential backoff:

| Attempt | Delay     |
| ------- | --------- |
| 1       | immediate |
| 2       | +30s      |
| 3       | +2m       |
| 4       | +10m      |
| 5       | +1h       |
| 6       | +6h       |

After the final failed attempt, the delivery is marked `failed` and no further retries occur. All attempts — successful or not — are recorded in the webhook logs (accessible via `GET /v1/webhooks/{id}/logs`).

<Note>
  Deliveries are **at-least-once**. Use the `X-LeadLex-Delivery-ID` header to deduplicate in your handler if exactly-once semantics matter.
</Note>

## Security

<Warning>
  **SSRF Protection.** LeadLex blocks webhook URLs that resolve to private IP ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, `169.254.0.0/16`, IPv6 link-local). Attempts to register such URLs are rejected with `400 invalid_url`.
</Warning>

Additional safeguards:

* **HTTPS required** — `http://` URLs are rejected at registration time.
* **Timeout** — deliveries hard-timeout at 10 seconds to prevent slowloris attacks on LeadLex workers.
* **Secret rotation** — delete and recreate a webhook to rotate its signing secret.
* **Replay protection** — reject requests where `X-LeadLex-Timestamp` is older than 5 minutes.

## Managing Webhooks

Use the API to register, update, and inspect webhooks:

<CardGroup cols={2}>
  <Card title="List webhooks" href="/api-reference/webhooks/list" icon="list">
    `GET /v1/webhooks`
  </Card>

  <Card title="Create webhook" href="/api-reference/webhooks/create" icon="plus">
    `POST /v1/webhooks`
  </Card>

  <Card title="Update webhook" href="/api-reference/webhooks/update" icon="pen">
    `PATCH /v1/webhooks/{id}`
  </Card>

  <Card title="Delete webhook" href="/api-reference/webhooks/delete" icon="trash">
    `DELETE /v1/webhooks/{id}`
  </Card>

  <Card title="Delivery logs" href="/api-reference/webhooks/logs" icon="scroll">
    `GET /v1/webhooks/{id}/logs`
  </Card>
</CardGroup>

## Testing Locally

During development, tunnel your local server to a public HTTPS URL:

```bash theme={null}
# Using ngrok
ngrok http 3000
# -> https://abc123.ngrok-free.app

# Register the tunneled URL
curl -X POST https://data.leadlex.com/functions/v1/api-gateway/v1/webhooks \
  -H "Authorization: Bearer wbk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok-free.app/leadlex/webhook",
    "events": ["contact.created", "deal.updated"],
    "description": "Local dev tunnel"
  }'
```

Then trigger an event in the LeadLex UI (create a contact, update a deal) and watch your local server receive the signed payload.
