API & Webhooks
Leads per Schnittstelle anlegen und Aenderungen an andere Systeme senden.
inDeal API: Leads per Schnittstelle anlegen und aktualisieren
Mit der inDeal API legst du Leads aus anderen Systemen an, liest sie, hältst sie synchron und schreibst Aktivitäten wie Notizen oder Anrufe in die Timeline. Alles, was du dafür brauchst, findest du unter Einstellungen -> API & Webhooks. Inhalt - Schlüssel erstellen - Grundlagen - Lead anlegen - Lead lesen - Lead aktualisieren - Leads auflisten - Aktivitäten - Felder - Doppelte Leads - Fehlercodes - Weiter Schlüssel erstellen 1. Öffne Einstellungen -> API & Webhooks. 2. Klick auf Neuen Schlüssel erstellen und vergib einen Namen, der das angebundene System beschreibt. 3. Kopiere den Schlüssel sofort. Kopiere den Schlüssel jetzt. Er wird nur einmal angezeigt und kann später nicht erneut abgerufen werden. Der Dialog zum Erstellen eines Schlüssels mit dem Namen CRM-Sync Die einmalige Anzeige deines neuen Schlüssels mit Kopieren-Knopf Danach zeigt die Liste nur noch die letzten vier Zeichen. Brauchst du den Schlüssel erneut, widerrufe den alten mit Widerrufen und erstelle einen neuen. Es sind bis zu 10 aktive Schlüssel möglich - so kannst du je System einen eigenen vergeben und einzeln widerrufen. Die Schlüsselliste mit dem maskierten Schlüssel CRM-Sync und den Verbindungsdaten darunter Grundlagen | Angabe | Wert | |---|---| | Base-URL | https://app.indeal.ai/api/v1 | | Authentifizierung | Header Authorization: Bearer indeal_... | | Format | JSON in Anfrage und Antwort | | Limit | 600 Anfragen pro Minute je Schlüssel | Über dem Limit antwortet die API mit Status 429 und dem Header Retry-After: 60 - warte dann eine Minute. Lead anlegen POST /leads legt einen Lead an. Mindestens eines von email oder linkedin_url (die Profil-URL des Kontakts) ist Pflicht. curl -X POST https://app.indeal.ai/api/v1/leads \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "full_name": "Julia Weber", "email": "julia.weber@muster.de", "company_name": "Muster GmbH", "role_title": "Geschaeftsfuehrerin" }' Antwort bei Erfolg (Status 201): { "status": "created", "lead": { "id": "0b0e...", "stage": "qualifying", "full_name": "Julia Weber", "email": "julia.weber@muster.de", "company_name": "Muster GmbH" } } Existiert der Lead schon, bekommst du Status 200 mit "status": "duplicate" und dem bestehenden Lead - siehe Doppelte Leads. Lead lesen GET /leads/{id} liefert einen einzelnen Lead. curl https://app.indeal.ai/api/v1/leads/0b0e... \ -H "Authorization: Bearer indeal_..." Antwort (Status 200): { "lead": { ... } }. Eine unbekannte ID liefert Status 404. Lead aktualisieren PATCH /leads/{id} ändert nur die Felder, die du schickst. null leert ein Feld. Mit stage wechselst du die Phase - die Verlaufshistorie in inDeal wird dabei automatisch geschrieben. custom_data wird zusammengeführt, nicht ersetzt: null als Wert löscht einen Key, alle anderen Keys bleiben. curl -X PATCH https://app.indeal.ai/api/v1/leads/0b0e... \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "stage": "meeting_scheduled", "next_step": "Demo vorbereiten" }' Antwort (Status 200): { "lead": { ... } }. Kollidiert die Änderung mit einem bestehenden Lead (E-Mail oder Profil-URL schon vergeben), kommt Status 409. Leads auflisten GET /leads liefert Leads seitenweise, sortiert nach Änderungszeitpunkt. curl "https://app.indeal.ai/api/v1/leads?updated_since=2026-09-01T00:00:00Z&limit=100" \ -H "Authorization: Bearer indeal_..." | Parameter | Bedeutung | |---|---| | updated_since | nur Leads, die seit diesem Zeitpunkt geändert wurden (ISO 8601) | | limit | 1 bis 200, Standard 100 | | cursor | next_cursor aus der vorigen Antwort | | order | asc (Standard, älteste Änderung zuerst) oder desc (neueste zuerst) | Antwort: { "leads": [...], "next_cursor": "..." }. Ist next_cursor null, bist du am Ende. Der Cursor merkt sich die Sortierrichtung, du musst order beim Blättern nicht wiederholen. Für laufende Synchronisation: regelmäßig mit updated_since anfragen und mit cursor blättern. Lead per E-Mail oder Profil-URL finden Du kennst die E-Mail-Adresse oder die Profil-URL und brauchst die id? Gib sie als Parameter mit. inDeal sucht dann genauso wie beim Anlegen: Groß- und Kleinschreibung spielen keine Rolle, bei der Profil-URL auch nicht https://, www oder ein Schrägstrich am Ende. curl "https://app.indeal.ai/api/v1/leads?email=julia.weber@muster.de" \ -H "Authorization: Bearer indeal_..." | Parameter | Bedeutung | |---|---| | email | Treffer über die E-Mail-Adresse | | linkedin_url | Treffer über die Profil-URL | Gibst du beide an, bekommst du jeden Lead, auf den mindestens eines passt. Die Antwort ist { "leads": [...] } ohne next_cursor, denn mehr als zwei Treffer kann es nicht geben. Passt nichts, ist die Liste leer. updated_since und cursor werden bei dieser Suche ignoriert. Eine ungültige E-Mail-Adresse oder URL liefert Status 400 mit dem Feldnamen. Aktivitäten Jeder Lead hat eine Timeline. Über die API legst du dort Einträge an und liest sie. Aktivität anlegen POST /leads/{id}/activities legt einen Timeline-Eintrag am Lead an. Schreibbar sind die Typen note (Notiz), call (Anruf), email (E-Mail) und meeting_scheduled (Termin). Mindestens eines von title oder body ist Pflicht. Fehlt title, bildet inDeal ihn aus den ersten 80 Zeichen von body. | Feld | Pflicht | Hinweis | |---|---|---| | type | ja | note, call, email, meeting_scheduled | | title | nein | max. 200 Zeichen | | body | nein | Freitext, max. 10000 Zeichen, etwa das Gesprächsergebnis | | channel | nein | linkedin, email, meet, call | | occurred_at | nein | Zeitpunkt (ISO 8601), Standard jetzt, nicht in der Zukunft | curl -X POST https://app.indeal.ai/api/v1/leads/0b0e.../activities \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "type": "call", "body": "Kurzes Telefonat, Demo nächste Woche", "channel": "call" }' Antwort bei Erfolg (Status 201): { "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 } } Der Eintrag erscheint sofort in der Timeline des Leads, und Anrufe sowie E-Mails zählen als Kontaktpunkt. Die Typen reply (Antwort aus einer Kampagne) und stage_change (Phasenwechsel) schreibt inDeal selbst; schickst du sie, kommt Status 400. Eine unbekannte Lead-ID liefert Status 404. Timeline lesen GET /leads/{id}/activities liefert alle Einträge des Leads, neueste zuerst, inklusive der nur lesbaren Typen reply und stage_change. | Parameter | Bedeutung | |---|---| | type | nur ein Typ: note, call, email, meeting_scheduled, reply, stage_change | | limit | 1 bis 200, Standard 50 | | cursor | next_cursor aus der vorigen Antwort | curl "https://app.indeal.ai/api/v1/leads/0b0e.../activities?type=reply&limit=50" \ -H "Authorization: Bearer indeal_..." Antwort: { "activities": [...], "next_cursor": "..." }. Bei Antworten aus Kampagnen steht der Text in quote. edited_at zeigt, dass ein Eintrag nachträglich bearbeitet wurde. Ändern oder Löschen per API gibt es nicht. Willst du über neue Einträge sofort informiert werden, statt die Timeline abzufragen, abonniere das Webhook-Ereignis activity.created - siehe inDeal Webhooks. Felder Pflichtregel: mindestens eines von email / linkedin_url beim Anlegen. Alles andere ist optional. | Feld | Typ | Hinweis | |---|---|---| | full_name | Text | Name des Kontakts | | role_title | Text | Position | | email | Text | Dedup-Anker | | phone, mobile, company_phone | Text | werden automatisch bereinigt | | linkedin_url | Text | Profil-URL, Dedup-Anker | | company_name | Text | Firma | | company_size | Ganzzahl | Mitarbeiterzahl | | industry | Text | Branche | | company_website, company_linkedin | Text | Firmen-Links | | street, zip, city, country | Text | Adresse | | next_step | Text | nächster Schritt | | next_step_due | Datum | Format YYYY-MM-DD | | est_value | Zahl | geschätzter Wert | | description | Text | Beschreibung | | lead_source | Text | Herkunft | | sentiment | Auswahl | positive, neutral, negative | | channel_direction | Auswahl | inbound, outbound | | lost_reason | Text | Verlustgrund | | stage | Auswahl | qualifying, schedule_meeting, meeting_scheduled, meeting_no_show, meeting_done, lead_lost | | custom_data | Objekt | eigene Felder, siehe Eigene Felder | Jede Antwort enthält zusätzlich url, den Link zur Lead-Seite in inDeal (nur lesend). Unbekannte Felder lehnt die API ab - die Fehlermeldung nennt das betroffene Feld. Interne Felder (etwa Anreicherungs-Status) gibt die API weder heraus noch nimmt sie sie an. Eigene Felder Eigene Felder legst du in inDeal unter Layout anpassen an, getrennt für Leads und Deals. Ihre Werte stehen im Objekt custom_data, der Schlüssel ist der API-Name des Feldes. Felder abrufen curl "https://app.indeal.ai/api/v1/custom-fields?entity=lead" \ -H "Authorization: Bearer indeal_..." entity ist Pflicht (lead oder deal). Die Antwort enthält alle aktiven Felder in der Reihenfolge des Layouts, jeweils mit key (API-Name), label, type, bei Auswahlfeldern options, bei Währung currency_code und der section. Ausgeblendete Felder fehlen. Wertformat pro Typ | type | Wert in custom_data | Beispiel | |---|---|---| | text | Text, max. 10.000 Zeichen, wird getrimmt | "Rückruf nach Messe" | | url | Link, nur http oder https; ohne Schema wird https:// ergänzt | "https://indeal.ai" | | number | Zahl | 42 | | currency | Zahl; die Währung steht in currency_code des Feldes | 5000 | | checkbox | true oder false | true | | date | YYYY-MM-DD | "2026-10-15" | | datetime | ISO 8601 mit Offset | "2026-10-15T09:30:00+02:00" | | dropdown | der API-Name einer Option, nicht ihr Name | "gold" | | multiselect | Liste von Options-API-Namen, doppelte werden entfernt | ["messe", "empfehlung"] | null löscht einen Wert, ebenso ein leerer Text oder eine leere Liste. Pro Request sind höchstens 50 Schlüssel in custom_data erlaubt. Prüfung beim Schreiben POST und PATCH auf Leads und Deals prüfen custom_data gegen die Felder des jeweiligen Moduls: - Unbekannte Schlüssel und Schlüssel ausgeblendeter Felder werden ignoriert und in warnings gemeldet. Das ist kein Fehler, der Rest wird gespeichert. { "lead": { ... }, "warnings": [{ "key": "partner_id", "code": "unknown_key" }, { "key": "alt", "code": "deleted_field" }] } - Ungültige Werte bekannter Felder ergeben Status 422 mit validation_error und details. Dann wird nichts geschrieben, auch keine anderen Felder aus demselben Request. { "error": { "code": "validation_error", "message": "custom_data: invalid values.", "details": [ { "key": "tarif", "code": "invalid_option" }, { "key": "budget", "code": "invalid_type" } ] } } Mögliche Werte für details.code: invalid_type, invalid_option, invalid_date, invalid_url, too_long, readonly_field. warnings ist in jeder Schreibantwort enthalten, als leere Liste, wenn nichts ignoriert wurde. Doppelte Leads Beim Anlegen prüft inDeal zuerst die Profil-URL, dann die E-Mail-Adresse - beide unabhängig von Groß- und Kleinschreibung. Gibt es bereits einen passenden Lead, wird KEIN neuer angelegt: du bekommst Status 200 mit "status": "duplicate" und dem bestehenden Lead samt seiner id. Das macht den Endpoint gefahrlos wiederholbar: dieselbe Anfrage zweimal zu senden erzeugt nie zwei Leads. Willst du den bestehenden Lead ändern, nutze die id aus der Antwort mit PATCH /leads/{id}. Fehlercodes Alle Fehler haben dieselbe Form: { "error": { "code": "...", "message": "..." } }. Die Meldung ist immer auf Englisch und für Menschen gedacht, prüfe in deinem Code den code. { "error": { "code": "not_found", "message": "Lead not found." } } | Code | Status | Bedeutung | |---|---|---| | unauthorized | 401 | Schlüssel fehlt, ist ungültig oder widerrufen | | rate_limited | 429 | Limit überschritten, warte die Sekunden aus Retry-After | | validation_error | 400 | Eingabe ungültig, die Meldung nennt das Feld | | validation_error | 422 | Ungültige Werte in custom_data, details nennt Feld und Grund, es wurde nichts geschrieben | | not_found | 404 | Lead existiert nicht oder liegt im Papierkorb | | conflict | 409 | Änderung kollidiert mit einem bestehenden Lead | | blacklisted | 409 | Person oder Firma steht auf der Blacklist, es wurde nichts angelegt | | internal | 500 | Unerwarteter Fehler, versuch es erneut | | internal | 503 | Blacklist-Prüfung vorübergehend nicht möglich, es wurde nichts angelegt, versuch es später erneut | Weiter - inDeal API: Deals und Auswertungen - inDeal Webhooks: Lead- und Deal-Änderungen an andere Systeme senden
inDeal API: Deals und Auswertungen
Mit der inDeal API legst du Deals zu deinen Leads an, führst sie durch die Phasen bis zum Abschluss und holst dir fertige Kennzahlen, ohne selbst durch Listen zu blättern. Schlüssel, Base-URL und Grundlagen sind dieselben wie bei den Leads: inDeal API: Leads per Schnittstelle anlegen und aktualisieren. Inhalt - Was ein Deal ist - Deal anlegen - Deal lesen und aktualisieren - Deals auflisten - Aktivitäten am Deal - Felder - Auswertungen - Kennzahlen im Einzelnen - Weiter Was ein Deal ist Ein Deal entsteht aus einem Lead und behält über lead_id seine Herkunft. Er läuft durch die Phasen send_offer (Angebot senden) und order_received (Auftrag erhalten) und endet in won (gewonnen) oder lost (verloren). Zu jedem Lead gibt es höchstens einen offenen Deal - dieselbe Regel wie in der App, siehe Vom Lead zum Deal und zurück. Deal anlegen POST /deals legt einen Deal an. lead_id ist Pflicht; der Lead muss in deinem Account existieren, sonst antwortet die API mit 404. curl -X POST https://app.indeal.ai/api/v1/deals \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "lead_id": "0b0e...", "value": 12000, "confidence": 40, "expected_close_date": "2026-10-15" }' Antwort mit Status 201: { "status": "created", "deal": { "id": "7c1d...", "lead_id": "0b0e...", "stage": "send_offer", "value": 12000, "confidence": 40, "weighted_value": 4800, "expected_close_date": "2026-10-15", "won_at": null, "lost_at": null, "url": "https://app.indeal.ai/pipeline/7c1d...", "lead": { "id": "0b0e...", "full_name": "Julia Weber", "company_name": "Muster GmbH", "email": "julia.weber@muster.de", "url": "https://app.indeal.ai/leads/0b0e..." } } } Drei Dinge passieren dabei automatisch: - Fehlt value, übernimmt der Deal den geschätzten Wert (est_value) des Leads. Fehlt confidence, gilt 25. - Der Lead gilt ab jetzt als konvertiert. Seine Phase bleibt unverändert. - Hat der Lead schon einen Deal, der nicht im Papierkorb liegt, wird kein zweiter angelegt. Du bekommst Status 200 mit "status": "duplicate" und dem bestehenden Deal. Prüfe also einfach status, statt vorher nachzusehen. Deal lesen und aktualisieren GET /deals/{id} liefert { "deal": { ... } }. Eine unbekannte ID oder ein Deal aus einem anderen Account ergibt 404. PATCH /deals/{id} ändert nur die Felder, die du schickst. null leert ein Feld. curl -X PATCH https://app.indeal.ai/api/v1/deals/7c1d... \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "stage": "won", "value": 15000 }' - stage wechselt die Phase. won setzt won_at, lost setzt lost_at auf jetzt. Die Historie der Phasenwechsel schreibt inDeal selbst. - Bei lost lohnt sich lost_reason - der Grund erscheint in der App und im Webhook-Ereignis deal.lost. - Änderst du next_step_due, verfällt eine Erinnerung, die in der App zu diesem Termin gesetzt war. Deals auflisten GET /deals liefert Deals seitenweise, sortiert nach der letzten Änderung. | Parameter | Bedeutung | |---|---| | stage | nur diese Phase | | lead_id | nur Deals dieses Leads | | updated_since | ISO 8601, nur Deals, die seitdem geändert wurden | | order | asc (Standard, älteste zuerst) oder desc | | limit | 1 bis 200, Standard 100 | | cursor | next_cursor aus der vorigen Antwort | Antwort: { "deals": [...], "next_cursor": "..." }. Ist next_cursor null, war das die letzte Seite. Für Summen und Zähler brauchst du diese Liste nicht - dafür gibt es die Auswertungen. Aktivitäten am Deal Deals haben eine eigene Timeline. POST /deals/{id}/activities und GET /deals/{id}/activities funktionieren genau wie die Aktivitäten am Lead: dieselben Typen (note, call, email, meeting_scheduled), dieselben Felder, dieselbe Seitensteuerung. Ein Eintrag am Deal hängt nur am Deal, nicht zusätzlich am Lead. Felder | Feld | Typ | Hinweis | |---|---|---| | id, created_at, updated_at, stage_entered_at | nur lesend | | | lead_id | ID | Herkunfts-Lead, beim Anlegen Pflicht | | stage | Auswahl | send_offer, order_received, won, lost | | value | Zahl | Dealvolumen | | confidence | Ganzzahl 0 bis 100 | Wahrscheinlichkeit in Prozent | | weighted_value | nur lesend | value mal confidence geteilt durch 100 | | expected_close_date | Datum YYYY-MM-DD | erwarteter Abschluss | | offer_sent_at, next_step_due | ISO 8601 | | | next_step, notes, lost_reason | Text | lost_reason nur beim Aktualisieren | | won_at, lost_at | nur lesend | gesetzt beim Wechsel auf won oder lost | | url | nur lesend | Link zur Deal-Seite in inDeal | | lead | nur lesend | id, full_name, company_name, email und url des Leads | | custom_data | Objekt | eigene Felder für Deals, gleiches Format und gleiche Prüfung wie bei Leads, siehe Eigene Felder; die Felder liefert GET /custom-fields?entity=deal | Nicht über die API erreichbar: die Zuordnung zu einer Person im Team, Erinnerungen und der Papierkorb. Auswertungen GET /reports/summary liefert fertige Kennzahlen für einen Zeitraum - Leads, Termine, Deals und Conversion in einer Antwort. Die Zahlen gelten für den ganzen Account, unabhängig davon, wem ein Lead zugeordnet ist. Alle Zeiten sind UTC. | Parameter | Bedeutung | |---|---| | from | Beginn, YYYY-MM-DD oder ISO 8601. Standard: der 1. des laufenden Monats | | to | Ende, YYYY-MM-DD (der Tag zählt mit) oder ISO 8601 (exklusiv). Standard: jetzt | | sections | kommagetrennt aus leads, meetings, deals, conversion. Standard: alle vier | Der Zeitraum darf höchstens 366 Tage lang sein, und from muss vor to liegen - sonst antwortet die API mit 400. curl "https://app.indeal.ai/api/v1/reports/summary?from=2026-09-01&to=2026-09-30§ions=deals,conversion" \ -H "Authorization: Bearer indeal_..." Antwort mit Status 200, hier alle vier Blöcke: { "period": { "from": "2026-09-01T00:00:00+00:00", "to": "2026-10-01T00:00:00+00:00" }, "leads": { "created": 12, "by_stage": [ { "stage": "qualifying", "count": 40, "est_value_sum": 380000 } ], "by_source_tool": [ { "source_tool": "api", "count": 12 } ], "by_lead_source": [ { "lead_source": "Webinar", "count": 7 } ] }, "meetings": { "scheduled": 5, "done": 3 }, "deals": { "created": 3, "by_stage": [ { "stage": "send_offer", "count": 4, "value_sum": 54000, "weighted_sum": 21600 } ], "pipeline_value": 54000, "pipeline_weighted": 21600, "won_count": 1, "won_value": 15000, "lost_count": 1, "avg_days_to_won": 12.5 }, "conversion": { "leads_created": 12, "meetings_scheduled": 5, "deals_created": 3, "deals_won": 1, "meeting_rate": 0.4167, "deal_rate": 0.6, "win_rate": 0.3333 } } Quoten sind Zahlen zwischen 0 und 1 mit vier Nachkommastellen. Leads und Deals im Papierkorb zählen nirgends mit. Kennzahlen im Einzelnen Zwei Arten von Zahlen stecken in der Antwort: Zähler für den Zeitraum (was ist zwischen from und to passiert) und Bestandszahlen von heute (wie sieht die Pipeline jetzt aus). by_stage, pipeline_value und pipeline_weighted sind Bestandszahlen, alles andere bezieht sich auf den Zeitraum. | Kennzahl | Bedeutung | |---|---| | period.from, period.to | der ausgewertete Zeitraum, from inklusive, to exklusiv | | leads.created | Leads, die im Zeitraum angelegt wurden | | leads.by_stage | alle aktuellen Leads je Phase mit Anzahl und Summe des geschätzten Werts | | leads.by_source_tool | im Zeitraum angelegte Leads je Quelle (zum Beispiel api, import, extension) | | leads.by_lead_source | im Zeitraum angelegte Leads je Herkunft aus dem Feld Lead-Quelle, die 20 häufigsten; leer heißt unknown | | meetings.scheduled | Leads, die im Zeitraum auf Termin vereinbart gesetzt wurden | | meetings.done | Leads, die im Zeitraum auf Termin stattgefunden gesetzt wurden | | deals.created | Deals, die im Zeitraum angelegt wurden | | deals.by_stage | alle aktuellen Deals je Phase mit Anzahl, Summe der Werte und gewichteter Summe | | deals.pipeline_value | Summe der Werte aller offenen Deals (Angebot senden, Auftrag erhalten), Stand heute | | deals.pipeline_weighted | dieselbe Summe, gewichtet mit der Wahrscheinlichkeit | | deals.won_count, deals.won_value | im Zeitraum gewonnene Deals, Anzahl und Summe der Werte | | deals.lost_count | im Zeitraum verlorene Deals | | deals.avg_days_to_won | durchschnittliche Tage vom Anlegen des Deals bis zum Gewinn, nur für die im Zeitraum gewonnenen Deals; null ohne gewonnene Deals | | conversion.leads_created | wie leads.created | | conversion.meetings_scheduled | wie meetings.scheduled | | conversion.deals_created | wie deals.created | | conversion.deals_won | wie deals.won_count | | conversion.meeting_rate | vereinbarte Termine geteilt durch angelegte Leads; null ohne Leads | | conversion.deal_rate | angelegte Deals geteilt durch vereinbarte Termine; null ohne Termine | | conversion.win_rate | gewonnene Deals geteilt durch angelegte Deals; null ohne Deals | Die Quoten in conversion rechnen mit den Zählern desselben Zeitraums. Sie sind deshalb kein echter Trichter einer Kohorte: ein Termin im September kann zu einem Lead aus dem August gehören. Weiter - inDeal API: Leads per Schnittstelle anlegen und aktualisieren - inDeal Webhooks: Lead- und Deal-Änderungen an andere Systeme senden - MCP: Claude, ChatGPT und Cursor verbinden
inDeal Webhooks: Lead- und Deal-Änderungen an andere Systeme senden
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 - Events - Was ankommt - Signatur prüfen - Zustellung und Wiederholung - Wenn dein Endpoint deaktiviert wurde - Testen und erneut senden - Secret erneuern - Zapier, Make und n8n - Weiter Webhook anlegen 1. Öffne Einstellungen -> API & Webhooks 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. Der Dialog zum Anlegen eines Webhooks mit Name und Ziel-URL Die einmalige Anzeige deines Webhook-Secrets nach dem Anlegen 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 Was ankommt Jede Zustellung ist ein POST mit JSON-Body: { "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: { "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: { "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: 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: 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. 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 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