Integracje

Webhooki

Webhooki wysyłają każdą zakończoną rozmowę na Twój serwer jako JSON, z podsumowaniem AI i danymi respondenta. Dzięki nim zaktualizujesz CRM, otworzysz zgłoszenia albo załadujesz wyniki do własnej bazy danych bez ręcznego eksportu.

Co jest wysyłane i kiedy#

Diaform wysyła jedno zdarzenie: conversation.completed. Jest wysyłane, gdy agent AI kończy rozmowę i podsumowanie jest gotowe, zwykle chwilę po ostatniej wiadomości.

  • Dla całej organizacji. Każdy endpoint dostaje zdarzenia ze wszystkich projektów w organizacji. Projekty rozróżnisz po polu agent_id w payloadzie.
  • Anulowane rozmowy też są wysyłane. Jeśli respondent poprosi o przerwanie, rozmowa kończy się jako anulowana, a Ty i tak dostajesz conversation.completed z "status": "cancelled". Sprawdź status, zanim uznasz odpowiedź za kompletną.
  • Porzucone rozmowy nie są wysyłane. Jeśli respondent zamknie stronę, zanim rozmowa się zakończy, nic nie zostanie wysłane.
  • Podgląd w panelu nie wysyła zdarzeń. Żeby przetestować webhook, dokończ rozmowę przez prawdziwy link projektu.

Plany i uprawnienia#

Webhooki są dostępne we wszystkich płatnych planach, ale nie w darmowym okresie próbnym. Każdy plan ma limit endpointów:

PlanEndpointy
Starter3
Pro5
Business10
Enterprise25

Endpointy mogą dodawać, wstrzymywać i usuwać tylko osoby z rolami Właściciel i Administrator. Zobacz Członkowie i role i Zarządzanie subskrypcją.

Dodawanie endpointu#

Otwórz webhooki

W pasku bocznym przejdź do Ustawienia → Webhooki.

Wpisz URL

Kliknij Dodaj endpoint, wklej URL w oknie Nowy endpoint webhooka i kliknij Dodaj endpoint. URL musi zaczynać się od https://.

Zapisz sekret

Okno Sekret webhooka pokazuje Twój sekret podpisu. Kliknij Kopiuj, zapisz go w zmiennych środowiskowych serwera, a potem kliknij Gotowe.

Każdy endpoint ma własny sekret w postaci whsec_ i 43 znaków. Możesz go później ponownie wyświetlić lub skopiować z wiersza Sekret podpisu przy endpoincie.

Wstrzymywanie i usuwanie endpointu#

  • Wstrzymanie: przestaw przełącznik w wierszu endpointu z Aktywny na Wstrzymany. Zdarzenia z czasu wstrzymania nie zostaną wysłane później.
  • Usunięcie: kliknij ikonę kosza, a potem Usuń endpoint, żeby potwierdzić.

Nie możesz edytować URL endpointu. Żeby przejść na nowy URL, dodaj nowy endpoint, zaktualizuj serwer jego sekretem, a potem usuń stary.

Dostarczanie#

ZachowanieSzczegóły
MetodaPOST, treść w JSON
RozsyłanieWysyłane jednocześnie do wszystkich aktywnych endpointów
Limit czasu5 sekund. Do tego czasu odpowiedz dowolnym statusem 2xx.
PonowieniaBrak. Po przekroczeniu czasu lub odpowiedzi innej niż 2xx żądanie nie jest wysyłane ponownie.
Dziennik dostarczeńNiedostępny w panelu
CzasJeśli przetwarzanie zakończonej rozmowy się nie powiedzie, Diaform ponawia je co 30 minut, maksymalnie 4 razy łącznie. Webhook może wtedy dotrzeć długo po zakończeniu rozmowy.

Najpierw odpowiedz, a czasochłonną pracę wykonaj potem, np. wrzucając zdarzenie do kolejki.

Podaj Diaform docelowy URL. Przekierowania są obsługiwane, ale 301 lub 302 zamienia POST na GET i treść żądania przepada.

Żądanie#

HTTP
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906
NagłówekWartość
Content-Typeapplication/json
X-Webhook-Eventconversation.completed
X-Webhook-SignatureHMAC-SHA256 surowej treści w zapisie hex, podpisany sekretem endpointu

Nie ma nagłówka ze znacznikiem czasu ani z ID dostarczenia. Używaj conversation_id jako klucza idempotencji: zapisuj obsłużone ID i pomijaj powtórzenia.

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"
}

Pola najwyższego poziomu#

PoleTypOpis
conversation_idstringID rozmowy. Używaj go do usuwania duplikatów.
agent_idstringID projektu, do którego należy rozmowa
organization_idstringID Twojej organizacji
statusstringcompleted lub cancelled
respondentobjectKim jest respondent, zobacz niżej
summaryobjectPodsumowanie AI, zobacz niżej
completed_atstringCzas zakończenia rozmowy w formacie ISO 8601

respondent#

PoleTypOpis
emailstring lub nullE-mail respondenta, jeśli jest znany
metadataobjectPuste dla bezpośrednich linków. W osadzonych rozmowach zawiera szczegóły osadzenia, np. "embed": true i embed_parent_origin.
identitiesarrayWszystko, co wiadomo o respondencie, jako type, value i source
identities[].typestringNa przykład email, name, identifier, user_id, account_id lub company
identities[].sourcestringowner: podane przez Ciebie, przez link z zaproszeniem lub identyfikator z osadzenia. ai: wyodrębnione z wiadomości respondenta. system: dodane przez Diaform.
invite_idstring lub nullID zaproszenia, gdy rozmowa zaczęła się od linku z zaproszeniem. Zobacz API zaproszeń respondentów.

summary#

PoleTypOpis
respondent_emailnullPrzestarzałe pole, zawsze null. Adresy e-mail odczytuj z identities.
respondent_identitiesarrayTożsamości respondenta jako type i value, bez source
answersarrayJeden wpis na każde pytanie
answers[].question_idstringID pytania w Twoim projekcie
answers[].question_textstringTreść pytania
answers[].summarystringKrótkie podsumowanie odpowiedzi
answers[].sentimentstringpositive, negative, neutral lub mixed
answers[].confidencestringNa ile pełna była odpowiedź na pytanie: high, medium lub low
answers[].quotesarray lub nullCiekawe cytaty respondenta. null, gdy ich nie ma.
overall_sentimentstringpositive, negative, neutral lub mixed
urgent_issuesarray, null lub brak polaKrytyczne problemy oznaczone przez AI
additional_insightsstring, null lub brak polaOpinie, które nie pasują do konkretnego pytania

Transkrypcja i nazwa projektu nie są dołączane. Jak Diaform zbiera tożsamości, opisujemy w artykule Identyfikacja respondentów.

Weryfikacja podpisu#

Sprawdzaj każde żądanie, zanim mu zaufasz. Diaform podpisuje dokładną treść żądania algorytmem HMAC-SHA256, z sekretem endpointu jako kluczem, i wysyła skrót w zapisie hex w nagłówku X-Webhook-Signature.

  1. Odczytaj surową treść jako bajty, przed jakimkolwiek parsowaniem JSON.
  2. Oblicz HMAC-SHA256 tych bajtów swoim sekretem, w zapisie hex.
  3. Odrzuć żądanie, jeśli brakuje nagłówka albo ma on inną długość.
  4. Porównaj obie wartości funkcją działającą w stałym czasie.

Używaj całego sekretu, łącznie z prefiksem whsec_, jako zwykłego ciągu UTF-8. Nie dekoduj go z 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);

Podłącz express.raw() do tej ścieżki. Jeśli express.json() sparsuje treść wcześniej, bajty się zmienią i żaden podpis nie przejdzie weryfikacji.

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

Gdy weryfikacja się nie udaje#

  • Podpisuj surowe bajty, a nie ponownie zserializowany obiekt JSON.
  • Używaj sekretu endpointu, który odebrał żądanie. Każdy endpoint ma własny.
  • Sprawdź, czy w zapisanym sekrecie nie ma zbędnych spacji ani znaków nowej linii.