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_idi 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". Tjekstatus, 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:
| Plan | Endpoints |
|---|---|
| Starter | 3 |
| Pro | 5 |
| Business | 10 |
| Enterprise | 25 |
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ærd | Detaljer |
|---|---|
| Metode | POST, JSON-body |
| Fordeling | Sendes til alle aktive endpoints på samme tid |
| Timeout | 5 sekunder. Svar med en hvilken som helst 2xx-status inden da. |
| Genforsøg | Ingen. En timeout eller et svar uden 2xx-status sendes ikke igen. |
| Leveringslog | Ikke tilgængelig i dashboardet |
| Timing | Hvis 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#
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906| Header | Værdi |
|---|---|
Content-Type | application/json |
X-Webhook-Event | conversation.completed |
X-Webhook-Signature | Hex 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#
{
"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#
| Felt | Type | Beskrivelse |
|---|---|---|
conversation_id | string | Samtalens ID. Brug det til at fjerne dubletter. |
agent_id | string | ID på det projekt, samtalen hører til |
organization_id | string | Din organisations ID |
status | string | completed eller cancelled |
respondent | object | Hvem respondenten er, se nedenfor |
summary | object | AI-opsummeringen, se nedenfor |
completed_at | string | Tidspunkt i ISO 8601 for, hvornår samtalen sluttede |
respondent#
| Felt | Type | Beskrivelse |
|---|---|---|
email | string eller null | Respondentens e-mail, hvis den kendes |
metadata | object | Tom for direkte links. For indlejrede samtaler indeholder den detaljer om indlejringen, f.eks. "embed": true og embed_parent_origin. |
identities | array | Alt, hvad der vides om respondenten, som type, value og source |
identities[].type | string | For eksempel email, name, identifier, user_id, account_id eller company |
identities[].source | string | owner: du har leveret den via et invitationslink eller en identifikator i indlejringen. ai: udtrukket fra respondentens beskeder. system: tilføjet af Diaform. |
invite_id | string eller null | Invitations-id, når samtalen kom fra et invitationslink. Se Respondentinvitations-API. |
summary#
| Felt | Type | Beskrivelse |
|---|---|---|
respondent_email | null | Forældet felt, altid null. Læs e-mails fra identities. |
respondent_identities | array | Respondentens identiteter som type og value, uden source |
answers | array | Én post pr. spørgsmål |
answers[].question_id | string | Spørgsmålets ID i dit projekt |
answers[].question_text | string | Spørgsmålets tekst |
answers[].summary | string | Kort opsummering af svaret |
answers[].sentiment | string | positive, negative, neutral eller mixed |
answers[].confidence | string | Hvor fuldt spørgsmålet blev besvaret: high, medium eller low |
answers[].quotes | array eller null | Udvalgte citater fra respondenten. null, når der ikke er nogen. |
overall_sentiment | string | positive, negative, neutral eller mixed |
urgent_issues | array, null eller udeladt | Kritiske problemer, AI'en har markeret |
additional_insights | string, null eller udeladt | Feedback, 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.
- Læs den rå body som bytes, før du parser JSON.
- Beregn HMAC-SHA256 af de bytes med din secret, som hex.
- Afvis forespørgslen, hvis headeren mangler eller har en anden længde.
- 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)#
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)#
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 "", 200Hvis 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.