Webhooks
Webhooks schicken jedes beendete Gespräch als JSON an deinen Server, mit der KI-Zusammenfassung und den Angaben zum Teilnehmer. Damit aktualisierst du dein CRM, legst Tickets an oder lädst Ergebnisse in deine eigene Datenbank, ohne irgendetwas von Hand zu exportieren.
Was ausgelöst wird und wann#
Diaform sendet ein Event: conversation.completed. Es wird ausgelöst, wenn der KI-Interviewer ein Gespräch beendet und die Zusammenfassung fertig ist, meist kurz nach der letzten Nachricht.
- Für die ganze Organisation. Jeder Endpunkt bekommt Events aus allen Projekten deiner Organisation. Mit
agent_idim Payload unterscheidest du die Projekte. - Abgebrochene Gespräche lösen auch aus. Will ein Teilnehmer aufhören, endet das Gespräch als abgebrochen, und du bekommst trotzdem
conversation.completed, mit"status": "cancelled". Prüfstatus, bevor du eine Antwort als vollständig behandelst. - Verlassene Gespräche lösen nicht aus. Schließt ein Teilnehmer die Seite, ohne dass das Gespräch endet, wird nichts gesendet.
- Vorschauen im Dashboard lösen nicht aus. Zum Testen schließ ein Gespräch über den echten Link des Projekts ab.
Tarife und Berechtigungen#
Webhooks gibt es in allen kostenpflichtigen Tarifen, nicht in der kostenlosen Testphase. Jeder Tarif hat ein Limit für Endpunkte:
| Tarif | Endpunkte |
|---|---|
| Starter | 3 |
| Pro | 5 |
| Business | 10 |
| Enterprise | 25 |
Nur Inhaber und Admins können Endpunkte hinzufügen, pausieren oder löschen. Siehe Mitglieder und Rollen und Abo verwalten.
Endpunkt hinzufügen#
Webhooks öffnen
Geh in der Seitenleiste zu Einstellungen → Webhooks.
URL eingeben
Klick auf Endpunkt hinzufügen, füge deine URL im Dialog Neuer Webhook-Endpunkt ein und klick auf Endpunkt hinzufügen. Die URL muss mit https:// beginnen.
Secret speichern
Der Dialog Webhook-Secret zeigt dein Signatur-Secret. Klick auf Kopieren, speichere es in der Umgebung deines Servers und klick dann auf Fertig.
Jeder Endpunkt hat sein eigenes Secret, im Format whsec_ gefolgt von 43 Zeichen. Du kannst es später in der Zeile Signatur-Secret des Endpunkts wieder anzeigen oder kopieren.
Endpunkt pausieren oder löschen#
- Pausieren: Stell den Schalter in der Zeile des Endpunkts von Aktiv auf Pausiert. Events, die während der Pause passieren, werden nicht nachträglich gesendet.
- Löschen: Klick auf das Papierkorbsymbol und dann zur Bestätigung auf Endpunkt löschen.
Die URL eines Endpunkts lässt sich nicht bearbeiten. Um zu einer neuen URL zu wechseln, füge einen neuen Endpunkt hinzu, hinterleg sein Secret auf deinem Server und lösch dann den alten.
Zustellung#
| Verhalten | Details |
|---|---|
| Methode | POST, JSON-Body |
| Verteilung | Wird gleichzeitig an alle aktiven Endpunkte gesendet |
| Timeout | 5 Sekunden. Antworte vorher mit einem beliebigen 2xx-Status. |
| Wiederholungen | Keine. Bei einem Timeout oder einer Antwort, die nicht 2xx ist, wird nicht erneut gesendet. |
| Zustellprotokoll | Im Dashboard nicht verfügbar |
| Zeitpunkt | Schlägt die Verarbeitung eines abgeschlossenen Gesprächs fehl, wiederholt Diaform sie alle 30 Minuten, insgesamt bis zu 4 Versuche. Der Webhook kann dann deutlich nach dem Ende des Gesprächs ankommen. |
Antworte zuerst und erledige langsame Arbeit danach, zum Beispiel indem du das Event in eine Queue legst.
Gib Diaform die endgültige URL. Weiterleitungen werden zwar befolgt, aber ein 301 oder 302 macht aus dem POST ein GET, und der Body geht verloren.
Anfrage#
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906| Header | Wert |
|---|---|
Content-Type | application/json |
X-Webhook-Event | conversation.completed |
X-Webhook-Signature | Hex-HMAC-SHA256 des Roh-Bodys, signiert mit dem Secret des Endpunkts |
Es gibt keinen Header mit Zeitstempel oder Zustell-ID. Nutze conversation_id als Idempotenzschlüssel: Speichere die IDs, die du schon verarbeitet hast, und überspringe Wiederholungen.
Payload#
{
"conversation_id": "0f8d43a2-6df9-49c8-bf5d-fd2b6ec2d9ee",
"agent_id": "b8acb3ab-1f3d-4a7e-a846-b7f493f86b6d",
"organization_id": "4d6ecdb4-8543-4adf-ae65-1f24106d5474",
"status": "completed",
"respondent": {
"email": "alex@acme.com",
"metadata": {},
"identities": [
{ "type": "email", "value": "alex@acme.com", "source": "owner" },
{ "type": "identifier", "value": "customer-123", "source": "owner" },
{ "type": "name", "value": "Alex Morgan", "source": "ai" }
],
"invite_id": "3a4f49c7-b8f4-4c44-8f74-fd8e4277c3e4"
},
"summary": {
"respondent_email": null,
"respondent_identities": [
{ "type": "email", "value": "alex@acme.com" },
{ "type": "identifier", "value": "customer-123" },
{ "type": "name", "value": "Alex Morgan" }
],
"answers": [
{
"question_id": "V1StGXR8_Z5jdHi6B-myT",
"question_text": "How did your first week with the product go?",
"summary": "Setup was quick, but Alex struggled to invite teammates.",
"sentiment": "mixed",
"confidence": "high",
"quotes": ["I had everything running in ten minutes."]
}
],
"overall_sentiment": "mixed",
"urgent_issues": ["Teammate invites fail for Google Workspace accounts."],
"additional_insights": "Alex would pay more for an API."
},
"completed_at": "2026-05-07T12:15:41.123Z"
}Felder der obersten Ebene#
| Feld | Typ | Beschreibung |
|---|---|---|
conversation_id | string | ID des Gesprächs. Nutze sie zum Deduplizieren. |
agent_id | string | ID des Projekts, zu dem das Gespräch gehört |
organization_id | string | ID deiner Organisation |
status | string | completed oder cancelled |
respondent | object | Wer der Teilnehmer ist, siehe unten |
summary | object | Die KI-Zusammenfassung, siehe unten |
completed_at | string | Zeitpunkt im Format ISO 8601, zu dem das Gespräch endete |
respondent#
| Feld | Typ | Beschreibung |
|---|---|---|
email | string oder null | Die E-Mail-Adresse des Teilnehmers, falls bekannt |
metadata | object | Bei direkten Links leer. Bei eingebetteten Gesprächen enthält es Angaben zur Einbettung wie "embed": true und embed_parent_origin. |
identities | array | Alles, was über den Teilnehmer bekannt ist, als type, value und source |
identities[].type | string | Zum Beispiel email, name, identifier, user_id, account_id oder company |
identities[].source | string | owner: von dir geliefert, über einen Einladungslink oder eine Kennung in der Einbettung. ai: aus den Nachrichten des Teilnehmers erkannt. system: von Diaform hinzugefügt. |
invite_id | string oder null | Einladungs-ID, wenn das Gespräch über einen Einladungslink kam. Siehe Teilnehmer-Einladungen-API. |
summary#
| Feld | Typ | Beschreibung |
|---|---|---|
respondent_email | null | Altes Feld, immer null. Lies E-Mail-Adressen aus identities. |
respondent_identities | array | Die Identitäten des Teilnehmers als type und value, ohne source |
answers | array | Ein Eintrag pro Frage |
answers[].question_id | string | ID der Frage in deinem Projekt |
answers[].question_text | string | Der Fragetext |
answers[].summary | string | Kurze Zusammenfassung der Antwort |
answers[].sentiment | string | positive, negative, neutral oder mixed |
answers[].confidence | string | Wie vollständig die Frage beantwortet wurde: high, medium oder low |
answers[].quotes | array oder null | Markante Zitate des Teilnehmers. null, wenn es keine gibt. |
overall_sentiment | string | positive, negative, neutral oder mixed |
urgent_issues | array, null oder nicht vorhanden | Kritische Probleme, die die KI markiert hat |
additional_insights | string, null oder nicht vorhanden | Feedback, das zu keiner bestimmten Frage passt |
Transkript und Projektname sind nicht enthalten. Wie Diaform Identitäten erfasst, steht unter Teilnehmer identifizieren.
Signatur prüfen#
Prüf jede Anfrage, bevor du ihr vertraust. Diaform signiert den exakten Request-Body mit HMAC-SHA256, nutzt dabei das Secret des Endpunkts als Schlüssel und sendet den Hex-Digest in X-Webhook-Signature.
- Lies den Roh-Body als Bytes, bevor du JSON parst.
- Berechne den HMAC-SHA256 dieser Bytes mit deinem Secret, als Hex.
- Lehne die Anfrage ab, wenn der Header fehlt oder eine andere Länge hat.
- Vergleiche die beiden Werte mit einer Funktion mit konstanter Laufzeit.
Nutze das ganze Secret, einschließlich des Präfixes whsec_, als normalen UTF-8-String. Dekodiere es nicht mit Base64.
Node.js (Express)#
const crypto = require("node:crypto");
const express = require("express");
const app = express();
const secret = process.env.DIAFORM_WEBHOOK_SECRET;
function isValidSignature(rawBody, signature) {
if (!Buffer.isBuffer(rawBody) || typeof signature !== "string") {
return false;
}
const expected = Buffer.from(
crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
);
const received = Buffer.from(signature);
return (
received.length === expected.length &&
crypto.timingSafeEqual(received, expected)
);
}
app.post(
"/webhooks/diaform",
express.raw({ type: "application/json" }),
(req, res) => {
if (!isValidSignature(req.body, req.get("X-Webhook-Signature"))) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200);
// Respond first, then do the slow work.
console.log(event.conversation_id, event.status);
}
);
app.listen(3000);Binde express.raw() an diese Route. Parst express.json() den Body vorher, ändern sich die Bytes, und jede Signatur schlägt fehl.
Python (Flask)#
import hashlib
import hmac
import json
import os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["DIAFORM_WEBHOOK_SECRET"].encode("utf-8")
def is_valid_signature(raw_body, signature):
if not signature:
return False
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode("utf-8"), signature.encode("utf-8"))
@app.post("/webhooks/diaform")
def diaform_webhook():
raw_body = request.get_data()
if not is_valid_signature(raw_body, request.headers.get("X-Webhook-Signature")):
return "Invalid signature", 401
event = json.loads(raw_body)
print(event["conversation_id"], event["status"])
return "", 200Wenn die Prüfung fehlschlägt#
- Signiere die Roh-Bytes, kein neu serialisiertes JSON-Objekt.
- Nutze das Secret des Endpunkts, der die Anfrage empfangen hat. Jeder Endpunkt hat sein eigenes.
- Prüf, ob im gespeicherten Secret überzählige Leerzeichen oder Zeilenumbrüche stecken.