
With webhooks inDeal reports lead and deal changes and new activities to your systems on its own - instead of you polling regularly. Every change arrives as a POST to a URL of your choice.

**Contents**
- [Create a webhook](#create-a-webhook)
- [Events](#events)
- [What arrives](#what-arrives)
- [Verify the signature](#verify-the-signature)
- [Delivery and retries](#delivery-and-retries)
- [If your endpoint was disabled](#if-your-endpoint-was-disabled)
- [Testing and resending](#testing-and-resending)
- [Rotating the secret](#rotating-the-secret)
- [Zapier, Make and n8n](#zapier-make-and-n8n)
- [Next](#next)


## Create a webhook

1. Open [Settings -> API & Webhooks](https://app.indeal.ai/settings/api) and go to the Webhooks section.
2. Click **Add webhook**. Enter a name and the target URL - it must start with `https://` and point to a publicly reachable server. Addresses on internal networks, `localhost`, non-standard ports or redirects are rejected. Your receiver has to accept POST requests there and must not redirect.
3. All nine events are preselected, four for leads and five for deals. If you only want some of them, open **Advanced: send only specific events** and untick the others.
4. After creating, your webhook is active and you see your webhook secret. If your receiver should verify that requests really come from inDeal, copy it now - it is shown only once. For n8n, Zapier or Make you usually do not need it. If you need it later after all, generate a new one from the list with **Rotate secret**, see [Rotating the secret](#rotating-the-secret).

![The dialog for creating a webhook with name and target URL](https://avhmrgajilnfyuvkhect.supabase.co/storage/v1/object/public/helpcenter/api-und-webhooks/webhook-create.png)

![The one-time display of your webhook secret after creating](https://avhmrgajilnfyuvkhect.supabase.co/storage/v1/object/public/helpcenter/api-und-webhooks/webhook-secret.png)

Up to 5 webhooks are possible - for example one per connected system.

Webhooks you created before the deal events existed do not receive them automatically. The list then shows **New: deal events available.** - click **Edit** and tick the events you need.

## Events

| Event | When |
|---|---|
| `lead.created` | A new lead is created, no matter how: manually, via import, via the extension, via the API or from campaigns |
| `lead.updated` | At least one lead field or a custom field changed |
| `lead.stage_changed` | The stage changed |
| `activity.created` | A new entry in the timeline of a lead or a deal: note, call, email, meeting or a reply from a campaign - whether added manually, via import, from the email sync or via the API |
| `deal.created` | A new deal is created, in the app or via the API |
| `deal.updated` | At least one deal field changed (value, confidence, closing date, next step, notes) or a custom field |
| `deal.stage_changed` | The deal stage changed, to whichever stage |
| `deal.won` | The deal was set to won - comes in addition to `deal.stage_changed` |
| `deal.lost` | The deal was set to lost - comes in addition to `deal.stage_changed`, with the lost reason |

A stage change only produces `lead.stage_changed` or `deal.stage_changed`, no additional `lead.updated` or `deal.updated` and no `activity.created`. Closing a deal produces two deliveries: `deal.stage_changed` plus `deal.won` or `deal.lost`. If you only care about the closing, select the last two and leave out `deal.stage_changed`.

![The event selection under Advanced with the four lead and five deal events in two columns](https://avhmrgajilnfyuvkhect.supabase.co/storage/v1/object/public/helpcenter/api-und-webhooks/webhook-create-advanced.png)

## What arrives

Every delivery is a POST with a JSON body:

```json
{
  "event": "lead.updated",
  "occurred_at": "2026-09-03T10:15:00+00:00",
  "lead_id": "0b0e...",
  "workspace_id": "aa70...",
  "data": {
    "id": "0b0e...",
    "full_name": "Julia Weber",
    "stage": "qualifying",
    "company_name": "Muster GmbH",
    "custom_data": {}
  },
  "changes": {
    "company_name": { "from": null, "to": "Muster GmbH" }
  }
}
```

- `data` contains the current state of the lead (the same fields as the API response).
- `changes` differs per event: for `lead.updated` it holds only the changed fields with old and new value, for `lead.stage_changed` it holds `{ "stage": { "from": "qualifying", "to": "meeting_scheduled" } }`, for `lead.created` and `activity.created` it is empty.
- Deliveries from the test button additionally carry `"test": true`.

For `activity.created` the object `activity` comes along - the same fields as when creating an activity via the API. `data` is the current state of the lead here as well:

```json
{
  "event": "activity.created",
  "occurred_at": "2026-09-03T14:00:00+00:00",
  "lead_id": "0b0e...",
  "workspace_id": "aa70...",
  "activity": {
    "id": "9f3a...",
    "type": "call",
    "title": "Short call, demo next week",
    "body": "Short call, demo next week",
    "quote": null,
    "channel": "call",
    "occurred_at": "2026-09-03T14:00:00+00:00",
    "created_at": "2026-09-03T14:00:01+00:00",
    "pinned": false,
    "edited_at": null
  },
  "data": {
    "id": "0b0e...",
    "full_name": "Julia Weber",
    "stage": "qualifying",
    "company_name": "Muster GmbH",
    "custom_data": {}
  },
  "changes": {}
}
```

`type` is one of `note`, `call`, `email`, `meeting_scheduled` or `reply`. For `reply` (a reply from a campaign) the reply text is in `quote`.

The `entity` field says what the entry belongs to: `lead` or `deal`. For an entry on a deal, `deal_id` is set, `lead_id` is empty and `data` is the current state of the deal.

Deal events look the same, just with `deal_id` and the deal in `data` - the same fields as the deal response of the API, including the lead it belongs to. The values of custom fields are in `custom_data` for leads and deals, the key is the field's API name. When a custom field changes, you get `lead.updated` or `deal.updated`. An example of a deal event:

```json
{
  "event": "deal.lost",
  "occurred_at": "2026-09-05T09:30:00+00:00",
  "deal_id": "7c1d...",
  "lead_id": "0b0e...",
  "workspace_id": "aa70...",
  "data": {
    "id": "7c1d...",
    "lead_id": "0b0e...",
    "stage": "lost",
    "value": 12000,
    "confidence": 40,
    "weighted_value": 0,
    "lost_reason": "Budget cut",
    "won_at": null,
    "lost_at": "2026-09-05T09:30:00+00:00",
    "lead": { "id": "0b0e...", "full_name": "Julia Weber", "company_name": "Muster GmbH", "email": "julia.weber@muster.de" }
  },
  "changes": {
    "stage": { "from": "send_offer", "to": "lost" },
    "lost_reason": { "from": null, "to": "Budget cut" }
  }
}
```

- For `deal.updated`, `changes` holds only the changed fields with old and new value.
- For `deal.stage_changed` and `deal.won` it holds the stage change, for `deal.lost` additionally `lost_reason`.
- For `deal.created`, `changes` is empty.

These headers come along:

| Header | Content |
|---|---|
| `X-Indeal-Signature` | `sha256=...` - the signature, see below |
| `X-Indeal-Timestamp` | Unix seconds of sending |
| `X-Indeal-Event` | the event type |
| `X-Indeal-Delivery` | unique id of the delivery |

## Verify the signature

The signature proves the request really comes from inDeal. The text `timestamp.body` is signed with your secret (HMAC-SHA256). Verify with a constant-time comparison and only accept timestamps at most 5 minutes old.

Node.js:

```js
const { createHmac, timingSafeEqual } = require("node:crypto");

function verify(secret, req, rawBody) {
  const ts = Number(req.headers["x-indeal-timestamp"]);
  if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const got = req.headers["x-indeal-signature"] ?? "";
  const a = Buffer.from(expected);
  const b = Buffer.from(got);
  return a.length === b.length && timingSafeEqual(a, b);
}
```

Python:

```python
import hashlib, hmac, time

def verify(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    mac = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256)
    return hmac.compare_digest("sha256=" + mac.hexdigest(), signature)
```

Important: verify against the RAW request body, not against re-serialized JSON - otherwise the comparison fails.

## Delivery and retries

Respond with a status between 200 and 299. Anything else, including no response within 10 seconds, counts as a failed attempt.

- inDeal tries each delivery up to 8 times, with growing gaps: 1, 2, 4, 8, 16, 32, 64 minutes.
- After the eighth failure the delivery is considered finally failed. You can see it in the **Recent deliveries** panel and push it again with **Resend**.
- If 50 deliveries fail in a row, inDeal disables the webhook automatically, see [If your endpoint was disabled](#if-your-endpoint-was-disabled). From 5 consecutive failures the list already shows a yellow hint.
- Rarely a delivery can arrive twice. Use the `X-Indeal-Delivery` header to detect duplicates.

## If your endpoint was disabled

If 50 deliveries fail in a row, inDeal switches the webhook off. From then on no events are sent to this address, not even new ones.

- **You hear about it right away.** All admins of the account receive an email with the name, target URL, the last error in plain words and the time. In the webhook list the webhook shows in red as **Disabled** with the same reason.
- **Fix the cause.** The most common case: the receiver responds with 404 or 405 because the URL is no longer valid or does not accept POST. With n8n this is typically a test URL with `/webhook-test/` in the path. It only works while the workflow is waiting for a test event. For continuous operation you need the production URL of the activated workflow, which contains `/webhook/` without `-test`.
- **Re-enable.** Click **Re-enable** in the list. The failure counter starts at zero and new events are delivered again. Deliveries that finally failed in the meantime can be pushed again with **Resend** in the **Recent deliveries** panel.

## Testing and resending

- **Send test** creates a sample delivery of type `lead.created` with `"test": true` in the body. It runs through the same path as real deliveries and reaches your receiver within a minute. This lets you check reachability and signature without creating a real lead. Up to 5 tests per minute and webhook are possible, and only while the webhook is active.
- The **Recent deliveries** panel shows status, event, attempts, HTTP code and time per delivery. **Resend** immediately pushes an undelivered or finally failed delivery again.

![Your webhook row with expanded deliveries: two successful and one finally failed with an error text](https://avhmrgajilnfyuvkhect.supabase.co/storage/v1/object/public/helpcenter/api-und-webhooks/webhook-list-deliveries.png)

## Rotating the secret

The secret is shown only once. If you lost it, or want to replace it on a schedule, you do not need to recreate the webhook:

1. Click **Rotate secret** in the webhook row and confirm.
2. inDeal shows the new secret exactly once. Copy it and store it with your receiver.

The old secret is invalid from the moment you click. Deliveries sent after that are signed with the new secret - if your receiver still holds the old one, its signature check fails until you replace it there. Name, URL, events and the delivery history stay unchanged. If your receiver does not verify the signature (for example Zapier, Make or n8n without a code step), it will not notice the rotation.

## Zapier, Make and n8n

All three tools offer webhook triggers that generate a URL:

1. Create a webhook trigger there (Zapier: "Catch Hook", Make and n8n: a webhook node) and copy the generated URL.
2. Enter that URL as the target URL in inDeal. Best keep all events selected and filter on the `event` field in the tool.
3. Click **Send test** and build the rest of your flow from the received sample.

Signature verification is optional with these tools - the URLs are long and random, you usually do not need the secret. If you still want to verify, use a code step with the sample above.

## Next

- [inDeal API: create and update leads via the interface](https://success.indeal.ai/hc/indeal/articles/indeal-api-create-and-update-leads)
