Integrationer

Webhooks

Webhooks sender hver afsluttet samtale til din server som JSON med AI-opsummeringen og oplysninger om respondenten. Brug dem til at opdatere dit CRM, oprette sager eller indlæse resultater i din egen database uden at eksportere noget manuelt.

Hvad der sendes, og hvornår#

Diaform sender én hændelse, conversation.completed. Den udløses, når AI-intervieweren afslutter en samtale, og opsummeringen er klar, normalt kort efter den sidste besked.

  • For hele organisationen. Alle endpoints får hændelser fra alle projekter i din organisation. Brug agent_id i payloaden til at skelne projekterne fra hinanden.
  • Annullerede samtaler udløser også. Hvis en respondent beder om at stoppe, slutter samtalen som annulleret, og du får stadig conversation.completed, med "status": "cancelled". Tjek status, før du behandler et svar som gennemført.
  • Forladte samtaler udløser ikke. Hvis en respondent lukker siden, uden at samtalen er slut, sendes der intet.
  • Forhåndsvisninger i dashboardet udløser ikke. For at teste skal du gennemføre en samtale via projektets rigtige link.

Planer og rettigheder#

Webhooks findes på alle betalte planer, men ikke i den gratis prøveperiode. Hver plan har en grænse for antallet af endpoints:

PlanEndpoints
Starter3
Pro5
Business10
Enterprise25

Kun ejere og admins kan tilføje, sætte på pause eller slette endpoints. Se Medlemmer og roller og Administrér dit abonnement.

Tilføj et endpoint#

Åbn webhooks

Gå til Indstillinger → Webhooks i sidepanelet.

Indtast din URL

Klik på Tilføj endpoint, indsæt din URL i dialogen Nyt webhook-endpoint, og klik på Tilføj endpoint. URL'en skal starte med https://.

Gem din secret

Dialogen Webhook-secret viser din signerings-secret. Klik på Kopiér, gem den i din servers miljø, og klik derefter på Færdig.

Hvert endpoint har sin egen secret i formatet whsec_ efterfulgt af 43 tegn. Du kan vise eller kopiere den igen senere fra linjen Signerings-secret på endpointets række.

Sæt et endpoint på pause, eller slet det#

  • Pause: skift kontakten på endpointets række fra Aktiv til På pause. Hændelser, der sker, mens det er på pause, sendes ikke senere.
  • Slet: klik på skraldespandsikonet og derefter på Slet endpoint for at bekræfte.

Du kan ikke redigere et endpoints URL. For at skifte til en ny URL tilføjer du et nyt endpoint, opdaterer din server med dets secret og sletter derefter det gamle.

Levering#

AdfærdDetaljer
MetodePOST, JSON-body
FordelingSendes til alle aktive endpoints på samme tid
Timeout5 sekunder. Svar med en hvilken som helst 2xx-status inden da.
GenforsøgIngen. En timeout eller et svar uden 2xx-status sendes ikke igen.
LeveringslogIkke tilgængelig i dashboardet
TimingHvis behandlingen af en gennemført samtale fejler, prøver Diaform behandlingen igen hvert 30. minut, op til 4 forsøg i alt. Webhooken kan så komme et godt stykke tid efter, at samtalen sluttede.

Svar først, og lav det langsomme arbejde bagefter, f.eks. ved at lægge hændelsen i en kø.

Giv Diaform den endelige URL. Omdirigeringer følges, men en 301 eller 302 gør POST til GET, og body'en går tabt.

Forespørgsel#

HTTP
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906
HeaderVærdi
Content-Typeapplication/json
X-Webhook-Eventconversation.completed
X-Webhook-SignatureHex HMAC-SHA256 af den rå body, signeret med endpointets secret

Der er ingen header med tidsstempel eller leverings-ID. Brug conversation_id som idempotensnøgle: gem de ID'er, du har behandlet, og spring gentagelser over.

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

Felter på øverste niveau#

FeltTypeBeskrivelse
conversation_idstringSamtalens ID. Brug det til at fjerne dubletter.
agent_idstringID på det projekt, samtalen hører til
organization_idstringDin organisations ID
statusstringcompleted eller cancelled
respondentobjectHvem respondenten er, se nedenfor
summaryobjectAI-opsummeringen, se nedenfor
completed_atstringTidspunkt i ISO 8601 for, hvornår samtalen sluttede

respondent#

FeltTypeBeskrivelse
emailstring eller nullRespondentens e-mail, hvis den kendes
metadataobjectTom for direkte links. For indlejrede samtaler indeholder den detaljer om indlejringen, f.eks. "embed": true og embed_parent_origin.
identitiesarrayAlt, hvad der vides om respondenten, som type, value og source
identities[].typestringFor eksempel email, name, identifier, user_id, account_id eller company
identities[].sourcestringowner: du har leveret den via et invitationslink eller en identifikator i indlejringen. ai: udtrukket fra respondentens beskeder. system: tilføjet af Diaform.
invite_idstring eller nullInvitations-id, når samtalen kom fra et invitationslink. Se Respondentinvitations-API.

summary#

FeltTypeBeskrivelse
respondent_emailnullForældet felt, altid null. Læs e-mails fra identities.
respondent_identitiesarrayRespondentens identiteter som type og value, uden source
answersarrayÉn post pr. spørgsmål
answers[].question_idstringSpørgsmålets ID i dit projekt
answers[].question_textstringSpørgsmålets tekst
answers[].summarystringKort opsummering af svaret
answers[].sentimentstringpositive, negative, neutral eller mixed
answers[].confidencestringHvor fuldt spørgsmålet blev besvaret: high, medium eller low
answers[].quotesarray eller nullUdvalgte citater fra respondenten. null, når der ikke er nogen.
overall_sentimentstringpositive, negative, neutral eller mixed
urgent_issuesarray, null eller udeladtKritiske problemer, AI'en har markeret
additional_insightsstring, null eller udeladtFeedback, der ikke passer til et bestemt spørgsmål

Transskriptionen og projektets navn er ikke med. Se Identifikation af respondenter for at læse, hvordan Diaform indsamler identiteter.

Verificér signaturen#

Tjek hver forespørgsel, før du stoler på den. Diaform signerer den nøjagtige body med HMAC-SHA256 med endpointets secret som nøgle og sender hex-digesten i X-Webhook-Signature.

  1. Læs den rå body som bytes, før du parser JSON.
  2. Beregn HMAC-SHA256 af de bytes med din secret, som hex.
  3. Afvis forespørgslen, hvis headeren mangler eller har en anden længde.
  4. Sammenlign de to værdier med en funktion, der kører i konstant tid.

Brug hele secret'en, inklusive præfikset whsec_, som en almindelig UTF-8-streng. Base64-dekod den ikke.

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

Montér express.raw() på denne route. Hvis express.json() parser body'en først, ændres bytes, og alle signaturer fejler.

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

Hvis verificeringen fejler#

  • Signér de rå bytes, ikke et JSON-objekt, der er serialiseret igen.
  • Brug secret'en fra det endpoint, der modtog forespørgslen. Hvert endpoint har sin egen.
  • Tjek for overflødige mellemrum eller linjeskift i den secret, du har gemt.