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_idw 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.completedz"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:
| Plan | Endpointy |
|---|---|
| Starter | 3 |
| Pro | 5 |
| Business | 10 |
| Enterprise | 25 |
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#
| Zachowanie | Szczegóły |
|---|---|
| Metoda | POST, treść w JSON |
| Rozsyłanie | Wysyłane jednocześnie do wszystkich aktywnych endpointów |
| Limit czasu | 5 sekund. Do tego czasu odpowiedz dowolnym statusem 2xx. |
| Ponowienia | Brak. Po przekroczeniu czasu lub odpowiedzi innej niż 2xx żądanie nie jest wysyłane ponownie. |
| Dziennik dostarczeń | Niedostępny w panelu |
| Czas | Jeś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#
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906| Nagłówek | Wartość |
|---|---|
Content-Type | application/json |
X-Webhook-Event | conversation.completed |
X-Webhook-Signature | HMAC-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#
{
"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#
| Pole | Typ | Opis |
|---|---|---|
conversation_id | string | ID rozmowy. Używaj go do usuwania duplikatów. |
agent_id | string | ID projektu, do którego należy rozmowa |
organization_id | string | ID Twojej organizacji |
status | string | completed lub cancelled |
respondent | object | Kim jest respondent, zobacz niżej |
summary | object | Podsumowanie AI, zobacz niżej |
completed_at | string | Czas zakończenia rozmowy w formacie ISO 8601 |
respondent#
| Pole | Typ | Opis |
|---|---|---|
email | string lub null | E-mail respondenta, jeśli jest znany |
metadata | object | Puste dla bezpośrednich linków. W osadzonych rozmowach zawiera szczegóły osadzenia, np. "embed": true i embed_parent_origin. |
identities | array | Wszystko, co wiadomo o respondencie, jako type, value i source |
identities[].type | string | Na przykład email, name, identifier, user_id, account_id lub company |
identities[].source | string | owner: podane przez Ciebie, przez link z zaproszeniem lub identyfikator z osadzenia. ai: wyodrębnione z wiadomości respondenta. system: dodane przez Diaform. |
invite_id | string lub null | ID zaproszenia, gdy rozmowa zaczęła się od linku z zaproszeniem. Zobacz API zaproszeń respondentów. |
summary#
| Pole | Typ | Opis |
|---|---|---|
respondent_email | null | Przestarzałe pole, zawsze null. Adresy e-mail odczytuj z identities. |
respondent_identities | array | Tożsamości respondenta jako type i value, bez source |
answers | array | Jeden wpis na każde pytanie |
answers[].question_id | string | ID pytania w Twoim projekcie |
answers[].question_text | string | Treść pytania |
answers[].summary | string | Krótkie podsumowanie odpowiedzi |
answers[].sentiment | string | positive, negative, neutral lub mixed |
answers[].confidence | string | Na ile pełna była odpowiedź na pytanie: high, medium lub low |
answers[].quotes | array lub null | Ciekawe cytaty respondenta. null, gdy ich nie ma. |
overall_sentiment | string | positive, negative, neutral lub mixed |
urgent_issues | array, null lub brak pola | Krytyczne problemy oznaczone przez AI |
additional_insights | string, null lub brak pola | Opinie, 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.
- Odczytaj surową treść jako bajty, przed jakimkolwiek parsowaniem JSON.
- Oblicz HMAC-SHA256 tych bajtów swoim sekretem, w zapisie hex.
- Odrzuć żądanie, jeśli brakuje nagłówka albo ma on inną długość.
- 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)#
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)#
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 "", 200Gdy 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.