Webhooky
Webhooky posílají každý dokončený rozhovor na váš server jako JSON, se shrnutím od AI a údaji o respondentovi. Můžete tak aktualizovat CRM, zakládat tikety nebo nahrávat výsledky do vlastní databáze, aniž byste cokoli ručně exportovali.
Co se odesílá a kdy#
Diaform posílá jednu událost, conversation.completed. Odešle se, když AI tazatel rozhovor ukončí a shrnutí je hotové, obvykle chvíli po poslední zprávě.
- Pro celou organizaci. Každý endpoint dostává události ze všech projektů ve vaší organizaci. Projekty rozlišíte podle
agent_idv datech. - Odesílají se i zrušené rozhovory. Když respondent požádá o ukončení, rozhovor skončí jako zrušený a vy i tak dostanete
conversation.completed, s"status": "cancelled". Než budete odpověď považovat za úplnou, zkontrolujtestatus. - Opuštěné rozhovory se neodesílají. Když respondent zavře stránku, aniž by rozhovor skončil, nic se nepošle.
- Náhledy z dashboardu se neodesílají. Pro test dokončete rozhovor přes skutečný odkaz projektu.
Tarify a oprávnění#
Webhooky jsou dostupné ve všech placených tarifech, během zkušební verze zdarma ne. Každý tarif má limit endpointů:
| Tarif | Endpointy |
|---|---|
| Starter | 3 |
| Pro | 5 |
| Business | 10 |
| Enterprise | 25 |
Endpointy mohou přidávat, pozastavovat a mazat jen vlastníci a správci. Viz Členové a role a Správa předplatného.
Přidání endpointu#
Otevřete webhooky
V postranním panelu přejděte na Nastavení → Webhooky.
Zadejte URL
Klikněte na Přidat endpoint, vložte URL do dialogu Nový endpoint webhooku a klikněte na Přidat endpoint. URL musí začínat na https://.
Uložte tajný klíč
Dialog Tajný klíč webhooku ukáže váš tajný klíč pro podpis. Klikněte na Kopírovat, uložte ho do prostředí svého serveru a pak klikněte na Hotovo.
Každý endpoint má vlastní tajný klíč ve tvaru whsec_, za kterým následuje 43 znaků. Později ho můžete znovu zobrazit nebo zkopírovat na řádku Tajný klíč pro podpis u daného endpointu.
Pozastavení nebo smazání endpointu#
- Pozastavení: přepněte přepínač na řádku endpointu z Aktivní na Pozastaveno. Události, které nastanou během pozastavení, se později nedoposílají.
- Smazání: klikněte na ikonu koše a potvrďte tlačítkem Smazat endpoint.
URL endpointu nejde upravit. Pokud chcete přejít na novou URL, přidejte nový endpoint, nastavte na serveru jeho tajný klíč a pak starý endpoint smažte.
Doručování#
| Chování | Podrobnosti |
|---|---|
| Metoda | POST, tělo v JSON |
| Rozesílání | Odesílá se všem aktivním endpointům současně |
| Časový limit | 5 sekund. Do té doby odpovězte jakýmkoli stavem 2xx. |
| Opakování | Žádné. Při vypršení časového limitu nebo odpovědi jiné než 2xx se událost znovu neposílá. |
| Protokol doručení | V dashboardu není k dispozici |
| Načasování | Pokud zpracování dokončeného rozhovoru selže, Diaform ho zkouší znovu každých 30 minut, celkem nejvýš 4 pokusy. Webhook pak může dorazit až dlouho po skončení rozhovoru. |
Nejdřív odpovězte a pomalou práci udělejte až potom, například tak, že událost zařadíte do fronty.
Zadejte do nástroje Diaform konečnou URL. Přesměrování se sice následují, ale 301 nebo 302 změní POST na GET a tělo požadavku se ztratí.
Požadavek#
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906| Hlavička | Hodnota |
|---|---|
Content-Type | application/json |
X-Webhook-Event | conversation.completed |
X-Webhook-Signature | Hex HMAC-SHA256 nezpracovaného těla, podepsaný tajným klíčem endpointu |
Hlavička s časovým razítkem nebo ID doručení neexistuje. Jako klíč idempotence použijte conversation_id: ukládejte si zpracovaná ID a opakování přeskakujte.
Data#
{
"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"
}Pole nejvyšší úrovně#
| Pole | Typ | Popis |
|---|---|---|
conversation_id | string | ID rozhovoru. Použijte ho k odstranění duplicit. |
agent_id | string | ID projektu, ke kterému rozhovor patří |
organization_id | string | ID vaší organizace |
status | string | completed nebo cancelled |
respondent | object | Kdo je respondent, viz níže |
summary | object | Shrnutí od AI, viz níže |
completed_at | string | Čas skončení rozhovoru ve formátu ISO 8601 |
respondent#
| Pole | Typ | Popis |
|---|---|---|
email | string nebo null | E-mail respondenta, pokud je známý |
metadata | object | U přímých odkazů prázdné. U vložených rozhovorů obsahuje údaje o vložení, například "embed": true a embed_parent_origin. |
identities | array | Vše, co je o respondentovi známo, jako type, value a source |
identities[].type | string | Například email, name, identifier, user_id, account_id nebo company |
identities[].source | string | owner: zadali jste ji vy, přes odkaz s pozvánkou nebo identifikátor ve vložení. ai: vytažená ze zpráv respondenta. system: přidaná systémem Diaform. |
invite_id | string nebo null | ID pozvánky, pokud rozhovor vznikl z odkazu s pozvánkou. Viz API pro pozvánky respondentů. |
summary#
| Pole | Typ | Popis |
|---|---|---|
respondent_email | null | Starší pole, vždy null. E-maily čtěte z identities. |
respondent_identities | array | Identity respondenta jako type a value, bez source |
answers | array | Jedna položka na každou otázku |
answers[].question_id | string | ID otázky ve vašem projektu |
answers[].question_text | string | Text otázky |
answers[].summary | string | Krátké shrnutí odpovědi |
answers[].sentiment | string | positive, negative, neutral nebo mixed |
answers[].confidence | string | Jak úplně respondent na otázku odpověděl: high, medium nebo low |
answers[].quotes | array nebo null | Výrazné citace respondenta. null, když žádné nejsou. |
overall_sentiment | string | positive, negative, neutral nebo mixed |
urgent_issues | array, null nebo chybí | Kritické problémy, které AI označila |
additional_insights | string, null nebo chybí | Zpětná vazba, která nepatří ke konkrétní otázce |
Přepis ani název projektu data neobsahují. Jak Diaform získává identity, se dočtete v článku Identifikace respondentů.
Ověření podpisu#
Každý požadavek si ověřte dřív, než mu budete věřit. Diaform podepisuje přesné tělo požadavku pomocí HMAC-SHA256 s tajným klíčem endpointu jako klíčem a hex výtah posílá v X-Webhook-Signature.
- Načtěte nezpracované tělo jako bajty, ještě před jakýmkoli parsováním JSON.
- Spočítejte z těchto bajtů HMAC-SHA256 se svým tajným klíčem, v hex.
- Pokud hlavička chybí nebo má jinou délku, požadavek odmítněte.
- Obě hodnoty porovnejte funkcí s konstantním časem.
Použijte celý tajný klíč včetně předpony whsec_ jako obyčejný řetězec v UTF-8. Nedekódujte ho 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);Na tuto cestu připojte express.raw(). Pokud tělo nejdřív zpracuje express.json(), bajty se změní a žádný podpis nebude sedět.
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 "", 200Když ověření selže#
- Podepisujte nezpracované bajty, ne znovu serializovaný objekt JSON.
- Použijte tajný klíč endpointu, který požadavek přijal. Každý endpoint má vlastní.
- Zkontrolujte, zda v uloženém tajném klíči nejsou nadbytečné mezery nebo zalomení řádků.