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_iddans 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érifiezstatusavant 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 :
| Offre | Endpoints |
|---|---|
| Starter | 3 |
| Pro | 5 |
| Business | 10 |
| Enterprise | 25 |
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#
| Comportement | Détail |
|---|---|
| Méthode | POST, corps JSON |
| Diffusion | Envoyé à tous les endpoints actifs en même temps |
| Délai d’expiration | 5 secondes. Répondez avec un statut 2xx avant la fin de ce délai. |
| Nouvelles tentatives | Aucune. Un dépassement de délai ou une réponse autre que 2xx n’entraîne pas de nouvel envoi. |
| Journal de livraison | Non disponible dans le tableau de bord |
| Délai d’arrivée | Si 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#
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906| En-tête | Valeur |
|---|---|
Content-Type | application/json |
X-Webhook-Event | conversation.completed |
X-Webhook-Signature | HMAC-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#
{
"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#
| Champ | Type | Description |
|---|---|---|
conversation_id | string | ID de la conversation. Utilisez-le pour dédoublonner. |
agent_id | string | ID du projet auquel appartient la conversation |
organization_id | string | L’ID de votre organisation |
status | string | completed ou cancelled |
respondent | object | Qui est le répondant, voir ci-dessous |
summary | object | Le résumé IA, voir ci-dessous |
completed_at | string | Heure de fin de la conversation, au format ISO 8601 |
respondent#
| Champ | Type | Description |
|---|---|---|
email | string ou null | L’e-mail du répondant, s’il est connu |
metadata | object | Vide 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. |
identities | array | Tout ce que l’on sait du répondant, sous la forme type, value et source |
identities[].type | string | Par exemple email, name, identifier, user_id, account_id ou company |
identities[].source | string | owner : 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_id | string ou null | ID de l’invitation, si la conversation vient d’un lien d’invitation. Voir API des invitations de répondants. |
summary#
| Champ | Type | Description |
|---|---|---|
respondent_email | null | Champ hérité, toujours null. Lisez les e-mails dans identities. |
respondent_identities | array | Les identités du répondant sous la forme type et value, sans source |
answers | array | Une entrée par question |
answers[].question_id | string | ID de la question dans votre projet |
answers[].question_text | string | Le texte de la question |
answers[].summary | string | Court résumé de la réponse |
answers[].sentiment | string | positive, negative, neutral ou mixed |
answers[].confidence | string | Dans quelle mesure la question a reçu une réponse complète : high, medium ou low |
answers[].quotes | array ou null | Citations marquantes du répondant. null s’il n’y en a pas. |
overall_sentiment | string | positive, negative, neutral ou mixed |
urgent_issues | array, null ou absent | Problèmes critiques signalés par l’IA |
additional_insights | string, null ou absent | Feedback 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.
- Lisez le corps brut sous forme d’octets, avant tout parsing JSON.
- Calculez le HMAC-SHA256 de ces octets avec votre secret, en hexadécimal.
- Rejetez la requête si l’en-tête est absent ou n’a pas la même longueur.
- 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)#
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)#
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 "", 200Si 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.