Intégrations

Webhooks

Les webhooks envoient chaque conversation terminée à votre serveur au format JSON, avec le résumé IA et les informations sur le répondant. Utilisez-les pour mettre à jour votre CRM, ouvrir des tickets ou charger les résultats dans votre propre base de données, sans rien exporter à la main.

Quels événements et quand#

Diaform envoie un seul événement, conversation.completed. Il se déclenche quand l’intervieweur IA met fin à une conversation et que le résumé est prêt, généralement quelques instants après le dernier message.

  • Pour toute l’organisation. Chaque endpoint reçoit les événements de tous les projets de votre organisation. Utilisez agent_id dans le payload pour distinguer les projets.
  • Les conversations annulées déclenchent aussi l’événement. Si un répondant demande d’arrêter, la conversation se termine comme annulée et vous recevez quand même conversation.completed, avec "status": "cancelled". Vérifiez status avant de traiter une réponse comme complète.
  • Les conversations abandonnées ne déclenchent rien. Si un répondant ferme la page sans que la conversation se termine, rien n’est envoyé.
  • Les aperçus du tableau de bord ne déclenchent rien. Pour tester, terminez une conversation via le vrai lien du projet.

Offres et autorisations#

Les webhooks sont disponibles avec toutes les offres payantes, mais pas pendant l’essai gratuit. Chaque offre a une limite d’endpoints :

OffreEndpoints
Starter3
Pro5
Business10
Enterprise25

Seuls les propriétaires et les admins peuvent ajouter, mettre en pause ou supprimer des endpoints. Consultez Membres et rôles et Gérer votre abonnement.

Ajouter un endpoint#

Ouvrir les webhooks

Dans la barre latérale, allez dans Paramètres → Webhooks.

Saisir votre URL

Cliquez sur Ajouter un endpoint, collez votre URL dans la fenêtre Nouvel endpoint de webhook, puis cliquez sur Ajouter un endpoint. L’URL doit commencer par https://.

Enregistrer le secret

La fenêtre Secret du webhook affiche votre secret de signature. Cliquez sur Copier, stockez-le dans l’environnement de votre serveur, puis cliquez sur Terminé.

Chaque endpoint a son propre secret, de la forme whsec_ suivi de 43 caractères. Vous pouvez l’afficher ou le copier à nouveau plus tard depuis la ligne Secret de signature de l’endpoint.

Mettre en pause ou supprimer un endpoint#

  • Mettre en pause : faites passer l’interrupteur de la ligne de l’endpoint de la position Actif à En pause. Les événements survenus pendant la pause ne sont pas envoyés plus tard.
  • Supprimer : cliquez sur l’icône de corbeille, puis sur Supprimer l’endpoint pour confirmer.

Vous ne pouvez pas modifier l’URL d’un endpoint. Pour passer à une nouvelle URL, ajoutez un nouvel endpoint, mettez à jour votre serveur avec son secret, puis supprimez l’ancien.

Livraison#

ComportementDétail
MéthodePOST, corps JSON
DiffusionEnvoyé à tous les endpoints actifs en même temps
Délai d’expiration5 secondes. Répondez avec un statut 2xx avant la fin de ce délai.
Nouvelles tentativesAucune. Un dépassement de délai ou une réponse autre que 2xx n’entraîne pas de nouvel envoi.
Journal de livraisonNon disponible dans le tableau de bord
Délai d’arrivéeSi le traitement d’une conversation terminée échoue, Diaform relance le traitement toutes les 30 minutes, jusqu’à 4 tentatives au total. Le webhook peut alors arriver bien après la fin de la conversation.

Répondez d’abord et effectuez les traitements longs ensuite, par exemple en plaçant l’événement dans une file d’attente.

Donnez à Diaform l’URL finale. Les redirections sont suivies, mais un 301 ou un 302 transforme le POST en GET et le corps est perdu.

Requête#

HTTP
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906
En-têteValeur
Content-Typeapplication/json
X-Webhook-Eventconversation.completed
X-Webhook-SignatureHMAC-SHA256 en hexadécimal du corps brut, signé avec le secret de l’endpoint

Il n’y a pas d’en-tête d’horodatage ni d’ID de livraison. Utilisez conversation_id comme clé d’idempotence : stockez les ID déjà traités et ignorez les doublons.

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

Champs de premier niveau#

ChampTypeDescription
conversation_idstringID de la conversation. Utilisez-le pour dédoublonner.
agent_idstringID du projet auquel appartient la conversation
organization_idstringL’ID de votre organisation
statusstringcompleted ou cancelled
respondentobjectQui est le répondant, voir ci-dessous
summaryobjectLe résumé IA, voir ci-dessous
completed_atstringHeure de fin de la conversation, au format ISO 8601

respondent#

ChampTypeDescription
emailstring ou nullL’e-mail du répondant, s’il est connu
metadataobjectVide pour les liens directs. Pour les conversations intégrées, contient des détails sur l’intégration, comme "embed": true et embed_parent_origin.
identitiesarrayTout ce que l’on sait du répondant, sous la forme type, value et source
identities[].typestringPar exemple email, name, identifier, user_id, account_id ou company
identities[].sourcestringowner : vous l’avez fourni, via un lien d’invitation ou un identifiant d’intégration. ai : extrait des messages du répondant. system : ajouté par Diaform.
invite_idstring ou nullID de l’invitation, si la conversation vient d’un lien d’invitation. Voir API des invitations de répondants.

summary#

ChampTypeDescription
respondent_emailnullChamp hérité, toujours null. Lisez les e-mails dans identities.
respondent_identitiesarrayLes identités du répondant sous la forme type et value, sans source
answersarrayUne entrée par question
answers[].question_idstringID de la question dans votre projet
answers[].question_textstringLe texte de la question
answers[].summarystringCourt résumé de la réponse
answers[].sentimentstringpositive, negative, neutral ou mixed
answers[].confidencestringDans quelle mesure la question a reçu une réponse complète : high, medium ou low
answers[].quotesarray ou nullCitations marquantes du répondant. null s’il n’y en a pas.
overall_sentimentstringpositive, negative, neutral ou mixed
urgent_issuesarray, null ou absentProblèmes critiques signalés par l’IA
additional_insightsstring, null ou absentFeedback qui ne relève d’aucune question précise

La transcription et le nom du projet ne sont pas inclus. Pour savoir comment Diaform recueille les identités, consultez Identifier les répondants.

Vérifier la signature#

Vérifiez chaque requête avant de lui faire confiance. Diaform signe le corps exact de la requête avec HMAC-SHA256, en utilisant le secret de l’endpoint comme clé, et envoie l’empreinte hexadécimale dans X-Webhook-Signature.

  1. Lisez le corps brut sous forme d’octets, avant tout parsing JSON.
  2. Calculez le HMAC-SHA256 de ces octets avec votre secret, en hexadécimal.
  3. Rejetez la requête si l’en-tête est absent ou n’a pas la même longueur.
  4. Comparez les deux valeurs avec une fonction à temps constant.

Utilisez le secret complet, préfixe whsec_ compris, comme simple chaîne UTF-8. Ne le décodez pas en 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);

Montez express.raw() sur cette route. Si express.json() analyse le corps avant, les octets changent et toutes les signatures échouent.

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

Si la vérification échoue#

  • Signez les octets bruts, pas un objet JSON resérialisé.
  • Utilisez le secret de l’endpoint qui a reçu la requête. Chaque endpoint a le sien.
  • Vérifiez que le secret stocké ne contient pas d’espaces ou de sauts de ligne parasites.