Integraties

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_id in 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". Controleer status voordat 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:

AbonnementEndpoints
Starter3
Pro5
Business10
Enterprise25

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#

GedragDetails
MethodePOST, JSON-body
SpreidingTegelijk verstuurd naar alle actieve endpoints
Time-out5 seconden. Antwoord voor die tijd met een willekeurige 2xx-status.
Nieuwe pogingenGeen. Een time-out of een antwoord dat geen 2xx is, wordt niet opnieuw verstuurd.
BezorglogNiet beschikbaar in het dashboard
TimingMislukt 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#

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

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

Velden op het hoogste niveau#

VeldTypeBeschrijving
conversation_idstringID van het gesprek. Gebruik het om dubbele events te herkennen.
agent_idstringID van het project waar het gesprek bij hoort
organization_idstringID van je organisatie
statusstringcompleted of cancelled
respondentobjectWie de respondent is, zie hieronder
summaryobjectDe AI-samenvatting, zie hieronder
completed_atstringISO 8601-tijd waarop het gesprek eindigde

respondent#

VeldTypeBeschrijving
emailstring of nullHet e-mailadres van de respondent, als dat bekend is
metadataobjectLeeg bij directe links. Bij ingesloten gesprekken bevat het embed-gegevens zoals "embed": true en embed_parent_origin.
identitiesarrayAlles wat over de respondent bekend is, als type, value en source
identities[].typestringBijvoorbeeld email, name, identifier, user_id, account_id of company
identities[].sourcestringowner: jij hebt het aangeleverd, via een uitnodigingslink of een embed-identificatie. ai: gehaald uit de berichten van de respondent. system: toegevoegd door Diaform.
invite_idstring of nullUitnodigings-ID als het gesprek via een uitnodigingslink is gestart. Zie Respondentuitnodigingen-API.

summary#

VeldTypeBeschrijving
respondent_emailnullVerouderd veld, altijd null. Lees e-mailadressen uit identities.
respondent_identitiesarrayDe identiteiten van de respondent als type en value, zonder source
answersarrayEén item per vraag
answers[].question_idstringID van de vraag in je project
answers[].question_textstringDe tekst van de vraag
answers[].summarystringKorte samenvatting van het antwoord
answers[].sentimentstringpositive, negative, neutral of mixed
answers[].confidencestringHoe volledig de vraag is beantwoord: high, medium of low
answers[].quotesarray of nullOpvallende citaten van de respondent. null als er geen zijn.
overall_sentimentstringpositive, negative, neutral of mixed
urgent_issuesarray, null of ontbreektKritieke problemen die de AI heeft gemarkeerd
additional_insightsstring, null of ontbreektFeedback 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.

  1. Lees de ruwe body als bytes, voordat je JSON gaat parsen.
  2. Bereken de HMAC-SHA256 van die bytes met je secret, als hex.
  3. Weiger het request als de header ontbreekt of een andere lengte heeft.
  4. 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)#

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

Koppel express.raw() aan deze route. Als express.json() de body eerst parset, veranderen de bytes en mislukt elke handtekening.

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

Als 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.