Webhooks
Webhooks sturen elk afgelopen gesprek als JSON naar je server, met de AI-samenvatting en gegevens van de respondent. Gebruik ze om je CRM bij te werken, tickets aan te maken of resultaten in je eigen database te laden, zonder iets met de hand te exporteren.
Wat er wordt verstuurd en wanneer#
Diaform verstuurt één event, conversation.completed. Dat gaat af als de AI-interviewer een gesprek beëindigt en de samenvatting klaar is, meestal kort na het laatste bericht.
- Voor de hele organisatie. Elk endpoint krijgt events van elk project in je organisatie. Gebruik
agent_idin de payload om projecten uit elkaar te houden. - Ook geannuleerde gesprekken. Vraagt een respondent om te stoppen, dan eindigt het gesprek als geannuleerd en krijg je toch
conversation.completed, met"status": "cancelled". Controleerstatusvoordat je een antwoord als afgerond behandelt. - Niet bij afgebroken gesprekken. Sluit een respondent de pagina zonder dat het gesprek is geëindigd, dan wordt er niets verstuurd.
- Niet bij voorbeelden in het dashboard. Om te testen, rond je een gesprek af via de echte link van het project.
Abonnementen en rechten#
Webhooks zijn beschikbaar bij alle betaalde abonnementen, niet tijdens de gratis proefperiode. Elk abonnement heeft een limiet voor endpoints:
| Abonnement | Endpoints |
|---|---|
| Starter | 3 |
| Pro | 5 |
| Business | 10 |
| Enterprise | 25 |
Alleen eigenaren en beheerders kunnen endpoints toevoegen, pauzeren of verwijderen. Zie Leden en rollen en Abonnement beheren.
Een endpoint toevoegen#
Open webhooks
Ga in de zijbalk naar Instellingen → Webhooks.
Vul je URL in
Klik op Endpoint toevoegen, plak je URL in het venster Nieuw webhook-endpoint en klik op Endpoint toevoegen. De URL moet beginnen met https://.
Bewaar het secret
Het venster Webhook-secret toont je secret voor ondertekening. Klik op Kopiëren, sla het op in de omgevingsvariabelen van je server en klik daarna op Klaar.
Elk endpoint heeft een eigen secret, in de vorm whsec_ gevolgd door 43 tekens. Je kunt het later opnieuw tonen of kopiëren via de regel Secret voor ondertekening in de rij van het endpoint.
Een endpoint pauzeren of verwijderen#
- Pauzeren: zet de schakelaar in de rij van het endpoint van Actief op Gepauzeerd. Events die plaatsvinden terwijl het gepauzeerd is, worden later niet alsnog verstuurd.
- Verwijderen: klik op het prullenbakpictogram en daarna op Endpoint verwijderen om te bevestigen.
Je kunt de URL van een endpoint niet bewerken. Wil je naar een nieuwe URL, voeg dan een nieuw endpoint toe, werk je server bij met het nieuwe secret en verwijder daarna het oude endpoint.
Bezorging#
| Gedrag | Details |
|---|---|
| Methode | POST, JSON-body |
| Spreiding | Tegelijk verstuurd naar alle actieve endpoints |
| Time-out | 5 seconden. Antwoord voor die tijd met een willekeurige 2xx-status. |
| Nieuwe pogingen | Geen. Een time-out of een antwoord dat geen 2xx is, wordt niet opnieuw verstuurd. |
| Bezorglog | Niet beschikbaar in het dashboard |
| Timing | Mislukt de verwerking van een afgerond gesprek, dan probeert Diaform de verwerking elke 30 minuten opnieuw, tot 4 pogingen in totaal. De webhook kan dan pas ruim na het einde van het gesprek aankomen. |
Antwoord eerst en doe traag werk daarna, bijvoorbeeld door het event in een wachtrij te zetten.
Geef Diaform de definitieve URL. Redirects worden gevolgd, maar een 301 of 302 maakt van de POST een GET, en dan gaat de body verloren.
Request#
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906| Header | Waarde |
|---|---|
Content-Type | application/json |
X-Webhook-Event | conversation.completed |
X-Webhook-Signature | Hex HMAC-SHA256 van de ruwe body, ondertekend met het secret van het endpoint |
Er is geen header met een tijdstempel of bezorg-ID. Gebruik conversation_id als idempotentiesleutel: bewaar de ID's die je al hebt verwerkt en sla herhalingen 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"
}Velden op het hoogste niveau#
| Veld | Type | Beschrijving |
|---|---|---|
conversation_id | string | ID van het gesprek. Gebruik het om dubbele events te herkennen. |
agent_id | string | ID van het project waar het gesprek bij hoort |
organization_id | string | ID van je organisatie |
status | string | completed of cancelled |
respondent | object | Wie de respondent is, zie hieronder |
summary | object | De AI-samenvatting, zie hieronder |
completed_at | string | ISO 8601-tijd waarop het gesprek eindigde |
respondent#
| Veld | Type | Beschrijving |
|---|---|---|
email | string of null | Het e-mailadres van de respondent, als dat bekend is |
metadata | object | Leeg bij directe links. Bij ingesloten gesprekken bevat het embed-gegevens zoals "embed": true en embed_parent_origin. |
identities | array | Alles wat over de respondent bekend is, als type, value en source |
identities[].type | string | Bijvoorbeeld email, name, identifier, user_id, account_id of company |
identities[].source | string | owner: jij hebt het aangeleverd, via een uitnodigingslink of een embed-identificatie. ai: gehaald uit de berichten van de respondent. system: toegevoegd door Diaform. |
invite_id | string of null | Uitnodigings-ID als het gesprek via een uitnodigingslink is gestart. Zie Respondentuitnodigingen-API. |
summary#
| Veld | Type | Beschrijving |
|---|---|---|
respondent_email | null | Verouderd veld, altijd null. Lees e-mailadressen uit identities. |
respondent_identities | array | De identiteiten van de respondent als type en value, zonder source |
answers | array | Eén item per vraag |
answers[].question_id | string | ID van de vraag in je project |
answers[].question_text | string | De tekst van de vraag |
answers[].summary | string | Korte samenvatting van het antwoord |
answers[].sentiment | string | positive, negative, neutral of mixed |
answers[].confidence | string | Hoe volledig de vraag is beantwoord: high, medium of low |
answers[].quotes | array of null | Opvallende citaten van de respondent. null als er geen zijn. |
overall_sentiment | string | positive, negative, neutral of mixed |
urgent_issues | array, null of ontbreekt | Kritieke problemen die de AI heeft gemarkeerd |
additional_insights | string, null of ontbreekt | Feedback die niet bij een specifieke vraag past |
Het transcript en de projectnaam zitten er niet in. Lees in Respondenten identificeren hoe Diaform identiteiten verzamelt.
De handtekening verifiëren#
Controleer elk request voordat je het vertrouwt. Diaform ondertekent de exacte body van het request met HMAC-SHA256, met het secret van het endpoint als sleutel, en stuurt de hex-digest in X-Webhook-Signature.
- Lees de ruwe body als bytes, voordat je JSON gaat parsen.
- Bereken de HMAC-SHA256 van die bytes met je secret, als hex.
- Weiger het request als de header ontbreekt of een andere lengte heeft.
- Vergelijk de twee waarden met een functie die in constante tijd werkt.
Gebruik het hele secret, inclusief het voorvoegsel whsec_, als gewone UTF-8-string. Decodeer het niet met 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);Koppel express.raw() aan deze route. Als express.json() de body eerst parset, veranderen de bytes en mislukt elke handtekening.
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 "", 200Als de verificatie mislukt#
- Onderteken de ruwe bytes, niet een opnieuw geserialiseerd JSON-object.
- Gebruik het secret van het endpoint dat het request heeft ontvangen. Elk endpoint heeft een eigen secret.
- Controleer of er geen losse spaties of regeleinden in het opgeslagen secret staan.