
Mit Webhooks meldet inDeal Lead- und Deal-Änderungen und neue Aktivitäten von sich aus an deine Systeme - statt dass du regelmäßig anfragen musst. Jede Änderung kommt als POST an eine URL deiner Wahl.

**Inhalt**
- [Webhook anlegen](#webhook-anlegen)
- [Events](#events)
- [Was ankommt](#was-ankommt)
- [Signatur prüfen](#signatur-pruefen)
- [Zustellung und Wiederholung](#zustellung-und-wiederholung)
- [Wenn dein Endpoint deaktiviert wurde](#wenn-dein-endpoint-deaktiviert-wurde)
- [Testen und erneut senden](#testen-und-erneut-senden)
- [Secret erneuern](#secret-erneuern)
- [Zapier, Make und n8n](#zapier-make-und-n8n)
- [Weiter](#weiter)


## Webhook anlegen

1. Öffne [Einstellungen -> API & Webhooks](https://app.indeal.ai/settings/api) und geh zum Abschnitt Webhooks.
2. Klick auf **Webhook hinzufügen**. Trag einen Namen und die Ziel-URL ein - sie muss mit `https://` beginnen und auf einen öffentlich erreichbaren Server zeigen. Adressen im internen Netz, `localhost`, abweichende Ports oder Umleitungen lehnt inDeal ab. Dein Empfänger muss dort POST-Anfragen annehmen und darf nicht weiterleiten.
3. Alle neun Events sind vorausgewählt, vier für Leads und fünf für Deals. Willst du nur bestimmte empfangen, öffne **Erweitert: Nur bestimmte Ereignisse senden** und wähle die anderen ab.
4. Nach dem Anlegen ist dein Webhook aktiv und du siehst dein Webhook-Secret. Wenn dein Empfänger prüfen soll, dass Anfragen wirklich von inDeal kommen, kopiere es jetzt - es wird nur einmal angezeigt. Für n8n, Zapier oder Make brauchst du es in der Regel nicht. Solltest du es später doch brauchen, erzeuge in der Liste mit **Secret erneuern** ein neues, siehe [Secret erneuern](#secret-erneuern).

![Der Dialog zum Anlegen eines Webhooks mit Name und Ziel-URL](https://avhmrgajilnfyuvkhect.supabase.co/storage/v1/object/public/helpcenter/api-und-webhooks/webhook-create.png)

![Die einmalige Anzeige deines Webhook-Secrets nach dem Anlegen](https://avhmrgajilnfyuvkhect.supabase.co/storage/v1/object/public/helpcenter/api-und-webhooks/webhook-secret.png)

Bis zu 5 Webhooks sind möglich - zum Beispiel je angebundenem System einer.

Webhooks, die du angelegt hast, bevor es die Deal-Events gab, empfangen sie nicht automatisch. In der Liste steht dann **Neu: Deal-Ereignisse verfügbar.** - klick auf **Bearbeiten** und hake die Events an, die du brauchst.

## Events

| Event | Wann |
|---|---|
| `lead.created` | Ein neuer Lead entsteht, egal auf welchem Weg: von Hand, per Import, über die Erweiterung, per API oder aus Kampagnen |
| `lead.updated` | Mindestens ein Lead-Feld oder ein eigenes Feld hat sich geändert |
| `lead.stage_changed` | Die Phase hat gewechselt |
| `activity.created` | Ein neuer Eintrag in der Timeline eines Leads oder Deals: Notiz, Anruf, E-Mail, Termin oder eine Antwort aus einer Kampagne - egal ob von Hand, per Import, aus dem E-Mail-Abgleich oder per API |
| `deal.created` | Ein neuer Deal entsteht, in der App oder per API |
| `deal.updated` | Mindestens ein Deal-Feld hat sich geändert (Wert, Wahrscheinlichkeit, Abschlussdatum, nächster Schritt, Notizen) oder ein eigenes Feld |
| `deal.stage_changed` | Die Deal-Phase hat gewechselt, egal wohin |
| `deal.won` | Der Deal wurde auf gewonnen gesetzt - kommt zusätzlich zu `deal.stage_changed` |
| `deal.lost` | Der Deal wurde auf verloren gesetzt - kommt zusätzlich zu `deal.stage_changed`, mit dem Verlustgrund |

Ein Phasenwechsel erzeugt nur `lead.stage_changed` beziehungsweise `deal.stage_changed`, kein zusätzliches `lead.updated` oder `deal.updated` und kein `activity.created`. Beim Abschluss eines Deals kommen zwei Zustellungen: `deal.stage_changed` und dazu `deal.won` oder `deal.lost`. Wenn dich nur der Abschluss interessiert, wähle die beiden letzten und lass `deal.stage_changed` weg.

![Die Ereignis-Auswahl unter Erweitert mit den vier Lead- und fünf Deal-Ereignissen in zwei Spalten](https://avhmrgajilnfyuvkhect.supabase.co/storage/v1/object/public/helpcenter/api-und-webhooks/webhook-create-advanced.png)

## Was ankommt

Jede Zustellung ist ein POST mit 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` enthält den aktuellen Stand des Leads (dieselben Felder wie die API-Antwort).
- `changes` unterscheidet sich je Event: bei `lead.updated` stehen dort nur die geänderten Felder mit altem und neuem Wert, bei `lead.stage_changed` steht `{ "stage": { "from": "qualifying", "to": "meeting_scheduled" } }`, bei `lead.created` und `activity.created` ist es leer.
- Zustellungen aus dem Test-Knopf tragen zusätzlich `"test": true`.

Bei `activity.created` kommt zusätzlich das Objekt `activity` mit - dieselben Felder wie beim Anlegen einer Aktivität per API. `data` ist auch hier der aktuelle Stand des Leads:

```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": "Kurzes Telefonat, Demo nächste Woche",
    "body": "Kurzes Telefonat, Demo nächste Woche",
    "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` ist einer von `note`, `call`, `email`, `meeting_scheduled` oder `reply`. Bei `reply` (Antwort aus einer Kampagne) steht der Antworttext in `quote`.

Das Feld `entity` sagt, woran der Eintrag hängt: `lead` oder `deal`. Bei einem Eintrag am Deal ist `deal_id` gesetzt, `lead_id` ist leer und `data` ist der aktuelle Stand des Deals.

Deal-Events sehen genauso aus, nur mit `deal_id` und dem Deal in `data` - dieselben Felder wie die Deal-Antwort der API, inklusive des zugehörigen Leads. Die Werte eigener Felder stehen bei Leads und Deals in `custom_data`, der Schlüssel ist der API-Name des Feldes. Ändert sich ein eigenes Feld, kommt `lead.updated` bzw. `deal.updated`. Ein Beispiel für ein 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 gestrichen",
    "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 gestrichen" }
  }
}
```

- Bei `deal.updated` stehen in `changes` nur die geänderten Felder mit altem und neuem Wert.
- Bei `deal.stage_changed` und `deal.won` steht dort der Phasenwechsel, bei `deal.lost` zusätzlich `lost_reason`.
- Bei `deal.created` ist `changes` leer.

Dazu kommen diese Header:

| Header | Inhalt |
|---|---|
| `X-Indeal-Signature` | `sha256=...` - die Signatur, siehe unten |
| `X-Indeal-Timestamp` | Unix-Sekunden des Versands |
| `X-Indeal-Event` | der Event-Typ |
| `X-Indeal-Delivery` | eindeutige ID der Zustellung |

## Signatur prüfen

Mit der Signatur stellst du sicher, dass die Anfrage wirklich von inDeal kommt. Signiert wird der Text `timestamp.body` mit deinem Secret (HMAC-SHA256). Prüfe mit einem zeitkonstanten Vergleich und akzeptiere nur Timestamps, die höchstens 5 Minuten alt sind.

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

Wichtig: prüfe gegen den ROHEN Anfragetext, nicht gegen den neu serialisierten JSON-Body - sonst schlägt der Vergleich fehl.

## Zustellung und Wiederholung

Antworte mit einem Status zwischen 200 und 299. Alles andere, auch keine Antwort innerhalb von 10 Sekunden, zählt als Fehlversuch.

- inDeal versucht jede Zustellung bis zu 8 Mal, mit wachsendem Abstand: 1, 2, 4, 8, 16, 32, 64 Minuten.
- Nach dem achten Fehlversuch gilt die Zustellung als endgültig gescheitert. Du siehst sie im Aufklapper **Letzte Zustellungen** und kannst sie dort mit **Erneut senden** noch einmal anstoßen.
- Schlagen 50 Zustellungen in Folge fehl, deaktiviert inDeal den Webhook automatisch, siehe [Wenn dein Endpoint deaktiviert wurde](#wenn-dein-endpoint-deaktiviert-wurde). Ab 5 Fehlversuchen in Folge zeigt die Liste schon einen gelben Hinweis.
- Selten kann eine Zustellung doppelt ankommen. Nutze den Header `X-Indeal-Delivery`, um Doppelte zu erkennen.

## Wenn dein Endpoint deaktiviert wurde

Schlagen 50 Zustellungen in Folge fehl, schaltet inDeal den Webhook ab. Ab dann werden keine Ereignisse mehr an diese Adresse geschickt, auch keine neuen.

- **Du erfährst es sofort.** Alle Admins des Accounts bekommen eine E-Mail mit Name, Ziel-URL, dem letzten Fehler in Klartext und dem Zeitpunkt. In der Webhook-Liste steht der Webhook rot als **Deaktiviert** mit demselben Grund.
- **Ursache beheben.** Der häufigste Fall: der Empfänger antwortet mit 404 oder 405, weil die URL nicht mehr stimmt oder kein POST annimmt. Bei n8n ist das typischerweise eine Test-URL mit `/webhook-test/` im Pfad. Sie funktioniert nur, solange der Workflow auf ein Test-Ereignis wartet. Für den Dauerbetrieb brauchst du die Production-URL des aktivierten Workflows, sie enthält `/webhook/` ohne `-test`.
- **Wieder aktivieren.** Klick in der Liste auf **Wieder aktivieren**. Der Fehlerzähler startet bei null, und neue Ereignisse werden wieder zugestellt. Zustellungen, die in der Zwischenzeit endgültig gescheitert sind, kannst du im Aufklapper **Letzte Zustellungen** mit **Erneut senden** nachholen.

## Testen und erneut senden

- **Test senden** erzeugt eine Beispiel-Zustellung vom Typ `lead.created` mit `"test": true` im Body. Sie läuft durch denselben Weg wie echte Zustellungen und erscheint innerhalb einer Minute beim Empfänger. So prüfst du Erreichbarkeit und Signatur, ohne einen echten Lead anzulegen. Bis zu 5 Tests pro Minute und Webhook sind möglich, und nur bei aktivem Webhook.
- Der Aufklapper **Letzte Zustellungen** zeigt je Zustellung Status, Event, Versuche, HTTP-Code und Zeitpunkt. **Erneut senden** stößt eine noch nicht zugestellte oder endgültig gescheiterte Zustellung sofort wieder an.

![Deine Webhook-Zeile mit aufgeklappten Zustellungen: zwei erfolgreiche und eine endgültig gescheiterte mit Fehlertext](https://avhmrgajilnfyuvkhect.supabase.co/storage/v1/object/public/helpcenter/api-und-webhooks/webhook-list-deliveries.png)

## Secret erneuern

Das Secret wird nur einmal angezeigt. Hast du es verloren, oder willst du es turnusmäßig austauschen, brauchst du den Webhook nicht neu anzulegen:

1. Klick in der Webhook-Zeile auf **Secret erneuern** und bestätige.
2. inDeal zeigt dir das neue Secret genau einmal. Kopiere es und hinterlege es bei deinem Empfänger.

Das alte Secret ist ab dem Klick ungültig. Zustellungen, die danach rausgehen, sind mit dem neuen Secret signiert - trägt dein Empfänger noch das alte, schlägt seine Signaturprüfung fehl, bis du es dort ausgetauscht hast. Name, URL, Events und die Zustell-Historie bleiben unverändert. Prüft dein Empfänger die Signatur nicht (zum Beispiel Zapier, Make oder n8n ohne Code-Schritt), merkt er von der Erneuerung nichts.

## Zapier, Make und n8n

Alle drei Werkzeuge bieten Webhook-Trigger, die eine URL erzeugen:

1. Lege dort einen Webhook-Trigger an (bei Zapier "Catch Hook", bei Make und n8n einen Webhook-Knoten) und kopiere die erzeugte URL.
2. Trag diese URL als Ziel-URL in inDeal ein. Die Events lässt du am besten alle ausgewählt und filterst im Werkzeug nach dem Feld `event`.
3. Klick auf **Test senden** und bau den Rest deines Ablaufs mit dem empfangenen Beispiel auf.

Die Signaturprüfung ist bei diesen Werkzeugen optional - die URLs sind lang und zufällig, das Secret brauchst du in der Regel nicht. Wenn du trotzdem prüfen willst, nutze einen Code-Schritt mit dem Beispiel von oben.

## Weiter

- [inDeal API: Leads per Schnittstelle anlegen und aktualisieren](https://success.indeal.ai/hc/indeal/articles/indeal-api-leads-anlegen-und-aktualisieren)
