Integrationen

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_id im 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üf status, 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:

TarifEndpunkte
Starter3
Pro5
Business10
Enterprise25

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#

VerhaltenDetails
MethodePOST, JSON-Body
VerteilungWird gleichzeitig an alle aktiven Endpunkte gesendet
Timeout5 Sekunden. Antworte vorher mit einem beliebigen 2xx-Status.
WiederholungenKeine. Bei einem Timeout oder einer Antwort, die nicht 2xx ist, wird nicht erneut gesendet.
ZustellprotokollIm Dashboard nicht verfügbar
ZeitpunktSchlä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#

HTTP
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906
HeaderWert
Content-Typeapplication/json
X-Webhook-Eventconversation.completed
X-Webhook-SignatureHex-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#

JSON
{
  "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#

FeldTypBeschreibung
conversation_idstringID des Gesprächs. Nutze sie zum Deduplizieren.
agent_idstringID des Projekts, zu dem das Gespräch gehört
organization_idstringID deiner Organisation
statusstringcompleted oder cancelled
respondentobjectWer der Teilnehmer ist, siehe unten
summaryobjectDie KI-Zusammenfassung, siehe unten
completed_atstringZeitpunkt im Format ISO 8601, zu dem das Gespräch endete

respondent#

FeldTypBeschreibung
emailstring oder nullDie E-Mail-Adresse des Teilnehmers, falls bekannt
metadataobjectBei direkten Links leer. Bei eingebetteten Gesprächen enthält es Angaben zur Einbettung wie "embed": true und embed_parent_origin.
identitiesarrayAlles, was über den Teilnehmer bekannt ist, als type, value und source
identities[].typestringZum Beispiel email, name, identifier, user_id, account_id oder company
identities[].sourcestringowner: 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_idstring oder nullEinladungs-ID, wenn das Gespräch über einen Einladungslink kam. Siehe Teilnehmer-Einladungen-API.

summary#

FeldTypBeschreibung
respondent_emailnullAltes Feld, immer null. Lies E-Mail-Adressen aus identities.
respondent_identitiesarrayDie Identitäten des Teilnehmers als type und value, ohne source
answersarrayEin Eintrag pro Frage
answers[].question_idstringID der Frage in deinem Projekt
answers[].question_textstringDer Fragetext
answers[].summarystringKurze Zusammenfassung der Antwort
answers[].sentimentstringpositive, negative, neutral oder mixed
answers[].confidencestringWie vollständig die Frage beantwortet wurde: high, medium oder low
answers[].quotesarray oder nullMarkante Zitate des Teilnehmers. null, wenn es keine gibt.
overall_sentimentstringpositive, negative, neutral oder mixed
urgent_issuesarray, null oder nicht vorhandenKritische Probleme, die die KI markiert hat
additional_insightsstring, null oder nicht vorhandenFeedback, 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.

  1. Lies den Roh-Body als Bytes, bevor du JSON parst.
  2. Berechne den HMAC-SHA256 dieser Bytes mit deinem Secret, als Hex.
  3. Lehne die Anfrage ab, wenn der Header fehlt oder eine andere Länge hat.
  4. 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)#

JavaScript
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)#

Python
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 "", 200

Wenn 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.