Integrace

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_id v 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, zkontrolujte status.
  • 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ů:

TarifEndpointy
Starter3
Pro5
Business10
Enterprise25

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
MetodaPOST, tělo v JSON
RozesíláníOdesílá se všem aktivním endpointům současně
Časový limit5 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#

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

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

Pole nejvyšší úrovně#

PoleTypPopis
conversation_idstringID rozhovoru. Použijte ho k odstranění duplicit.
agent_idstringID projektu, ke kterému rozhovor patří
organization_idstringID vaší organizace
statusstringcompleted nebo cancelled
respondentobjectKdo je respondent, viz níže
summaryobjectShrnutí od AI, viz níže
completed_atstringČas skončení rozhovoru ve formátu ISO 8601

respondent#

PoleTypPopis
emailstring nebo nullE-mail respondenta, pokud je známý
metadataobjectU přímých odkazů prázdné. U vložených rozhovorů obsahuje údaje o vložení, například "embed": true a embed_parent_origin.
identitiesarrayVše, co je o respondentovi známo, jako type, value a source
identities[].typestringNapříklad email, name, identifier, user_id, account_id nebo company
identities[].sourcestringowner: 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_idstring nebo nullID pozvánky, pokud rozhovor vznikl z odkazu s pozvánkou. Viz API pro pozvánky respondentů.

summary#

PoleTypPopis
respondent_emailnullStarší pole, vždy null. E-maily čtěte z identities.
respondent_identitiesarrayIdentity respondenta jako type a value, bez source
answersarrayJedna položka na každou otázku
answers[].question_idstringID otázky ve vašem projektu
answers[].question_textstringText otázky
answers[].summarystringKrátké shrnutí odpovědi
answers[].sentimentstringpositive, negative, neutral nebo mixed
answers[].confidencestringJak úplně respondent na otázku odpověděl: high, medium nebo low
answers[].quotesarray nebo nullVýrazné citace respondenta. null, když žádné nejsou.
overall_sentimentstringpositive, negative, neutral nebo mixed
urgent_issuesarray, null nebo chybíKritické problémy, které AI označila
additional_insightsstring, 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.

  1. Načtěte nezpracované tělo jako bajty, ještě před jakýmkoli parsováním JSON.
  2. Spočítejte z těchto bajtů HMAC-SHA256 se svým tajným klíčem, v hex.
  3. Pokud hlavička chybí nebo má jinou délku, požadavek odmítněte.
  4. 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)#

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);

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

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

Když 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ů.