Centre d'aide
Chat API

Webhooks

Dernière mise à jour:

Aperçu des webhooks

Les webhooks permettent à ChatLab de notifier vos systèmes dès qu'un événement se produit dans vos chatbots. Au lieu d'interroger la Management API par polling ou d'exporter des données manuellement, vous enregistrez un point de terminaison HTTPS et ChatLab lui envoie une requête HTTP POST signée en temps réel - lorsqu'un visiteur laisse un lead, soumet un formulaire de contact, évalue une conversation, demande un agent humain ou lorsqu'une action IA s'exécute.

Utilisations fréquentes :

  • transférer les nouveaux leads directement vers votre CRM à la seconde où ils sont capturés
  • alerter votre équipe dans Slack lorsqu'un visiteur demande le chat en direct
  • intégrer les évaluations et résumés de conversation dans vos propres outils d'analyse
  • suivre l'exécution des actions IA et signaler les erreurs

Disponibilité : votre compte doit inclure la fonctionnalité Webhooks.

Où les configurer : dans l'application d'administration, ouvrez Paramètres du compte -> Webhooks (Account settings -> Webhooks, juste à côté de la section Management API). Les webhooks sont configurés au niveau du compte - un seul point de terminaison peut recevoir des événements provenant de tous vos bots ou d'un bot sélectionné.

Configuration d'un endpoint

  1. Ouvrez les Paramètres du compte -> Webhooks (Account settings -> Webhooks) et cliquez sur Ajouter un endpoint (Add endpoint).
  2. Remplissez le formulaire de l'endpoint :
    • Nom (Name) - un libellé pour votre propre référence, par ex. « Synchronisation CRM » ou « Alertes Slack ».
    • URL - l'adresse HTTPS vers laquelle ChatLab enverra les événements par requête POST.
    • Événements (Events) - sélectionnez les types d'événements que cet endpoint reçoit (voir le catalogue ci-dessous). Sélectionnez uniquement ce dont vous avez besoin ; les événements à fort volume comme ai_action.executed peuvent générer beaucoup de trafic.
    • Filtre de bot (Bot filter) (facultatif) - sélectionnez un bot, ou Tous les bots (All bots) pour tous les bots de votre compte.
    • Filtre de formulaire personnalisé (Custom form filter) (facultatif) - achemine les soumissions d'un formulaire personnalisé vers cet endpoint. Il restreint uniquement l'événement custom_form.submitted ; tous les autres événements auxquels vous êtes abonné (leads, demandes de contact, conversations, live chat, actions de l'IA) sont transmis indépendamment de ce paramètre.
  3. Validez. Le secret de l'endpoint s'affiche exactement une fois dans la boîte de dialogue de confirmation - copiez-le dès maintenant et conservez-le en lieu sûr. Vous en aurez besoin pour vérifier les signatures (voir Sécurité ci-dessous). Le texte en clair ne pourra pas être récupéré ultérieurement.

Chaque endpoint dispose également de :

  • Bouton d'activation/désactivation (Enable/disable toggle) - suspendez les distributions sans supprimer l'endpoint. Les endpoints désactivés ignorent silencieusement les événements (ils ne sont pas mis en file d'attente pour plus tard).
  • Envoyer un exemple d'événement (Send sample event) - transmet une requête de test signée à votre URL afin que vous puissiez vérifier votre récepteur de bout en bout. Vous pouvez choisir le type d'événement et modifier les valeurs d'exemple avant l'envoi, afin que votre gestionnaire voie des données réalistes. Le test arrive comme une distribution normale avec eventType correspondant à votre sélection (ou comme webhook.test pour une simple vérification de connectivité).
  • Renouveler le secret (Roll secret) - génère un nouveau secret et invalide l'ancien. Utilisez cette option si le secret a pu être divulgué. Le nouveau secret n'est affiché qu'une seule fois. Pour une rotation contrôlée, suspendez l'endpoint, renouvelez et copiez le nouveau secret, mettez à jour le récepteur, puis réactivez-le et envoyez un exemple d'événement. Les événements survenus pendant la suspension ne sont pas mis en file d'attente. Prévoyez cette interruption avant la rotation.
  • Journal des distributions (Delivery log) - une liste des distributions récentes par endpoint comprenant l'horodatage, le type d'événement, le statut HTTP renvoyé par votre serveur et le temps de réponse. Les échecs de distribution et les interruptions du disjoncteur (circuit breaker) sont visibles ici. Le journal est conservé pendant 14 jours.

Enveloppe de l'événement

Chaque envoi est une requête HTTP POST avec Content-Type: application/json. Le corps possède toujours la même enveloppe ; l'objet data est spécifique au type d'événement :

{
  "eventId": "9f1c1c8e-6a2b-4b9e-9d2f-3f8a1e2b4c5d",
  "eventType": "lead.created",
  "timestamp": "2026-08-13T14:22:31Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": { }
}
  • eventId - unique par événement. Utilisez-le pour la déduplication si votre traitement doit être idempotent.
  • eventType - l'un des types documentés ci-dessous ; également envoyé dans l'en-tête X-ChatLab-Event.
  • timestamp - heure UTC ISO 8601 à laquelle l'enveloppe d'envoi a été préparée.
  • botId / botName - le bot auquel appartient l'événement.
  • conversationId / sessionId - le contexte de la conversation, le cas échéant.

Catalogue des événements

lead.created

Se déclenche lorsqu'un visiteur soumet ses coordonnées - via le formulaire de collecte de leads, le pré-formulaire de chat en direct ou un formulaire personnalisé utilisé pour la collecte de leads.

{
  "eventId": "9f1c1c8e-6a2b-4b9e-9d2f-3f8a1e2b4c5d",
  "eventType": "lead.created",
  "timestamp": "2026-08-13T14:22:31Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "email": "jane.doe@example.com",
    "name": "Jane Doe",
    "phone": "+1 555 0123",
    "source": "LEAD_COLLECTION_FORM",
    "formCodeName": "lead_form",
    "formName": "Lead form",
    "fields": [
      {"name": "email", "value": "jane.doe@example.com", "type": "email"},
      {"name": "company", "value": "Acme Inc.", "type": "text"},
      {"name": "topics", "value": ["Billing", "Delivery"], "type": "multichoice"},
      {"name": "attachment", "value": "https://api.chatlab.com/aichat/customform/download?key=...&token=...", "type": "file"}
    ],
    "pageUrl": "https://acme.com/pricing"
  }
}
  • source - la manière dont les coordonnées ont été collectées : LEAD_COLLECTION_FORM (formulaire de collecte de leads), LIVE_CHAT_FORM (pré-formulaire de chat en direct), CONVERSATION (l'IA a relevé les coordonnées pendant l'échange), ADMIN_DATA_UPDATE ou UPDATE_CLIENT_CONTEXT (modifié du côté de ChatLab). Les soumissions du formulaire d'assistance humaine ne déclenchent jamais cet événement - elles déclenchent à la place contact_form.submitted.
  • email, name, phone - les coordonnées associées à la fiche du lead.
  • Lorsqu'un formulaire personnalisé est utilisé pour la collecte de leads, chaque champ défini sur ce formulaire est inclus dans fields, selon l'ordre du formulaire, et formCodeName / formName identifient le formulaire. Avec le formulaire de lead classique, tous deux valent null et fields est un tableau vide.
  • Chaque entrée dans fields est un objet {name, value, type}. name correspond au nom technique du champ, qui reste inchangé lors des modifications d'intitulé - utilisez-le pour le mappage dans votre CRM.
  • Pour les champs multichoice, value est un tableau des options sélectionnées. Les champs de type case à cocher constituent des entrées individuelles avec les valeurs "true" / "false".
  • Pour les champs file, value est un lien de téléchargement vers le fichier importé ; le webhook ne transporte jamais le contenu des fichiers.
  • pageUrl - la page sur laquelle se trouvait le visiteur au moment de la soumission.

contact_form.submitted

Se déclenche lorsqu'un visiteur soumet le formulaire de contact avec un conseiller humain ou un formulaire personnalisé utilisé pour le contact humain.

{
  "eventId": "3a7b9c2d-1e4f-4a6b-8c0d-5e2f7a9b1c3d",
  "eventType": "contact_form.submitted",
  "timestamp": "2026-08-13T14:25:02Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "email": "jane.doe@example.com",
    "message": "I need help with my last invoice.",
    "source": "CUSTOM_FORM",
    "formCodeName": "contact_form",
    "formName": "Contact form",
    "fields": [
      {"name": "email", "value": "jane.doe@example.com", "type": "email"},
      {"name": "order_number", "value": "A-10293", "type": "text"},
      {"name": "message", "value": "I need help with my last invoice.", "type": "textarea"}
    ]
  }
}
  • email - l'adresse laissée par le visiteur, à laquelle votre équipe d'assistance doit répondre.
  • source - CUSTOM_FORM lorsqu'un formulaire personnalisé sert de formulaire de contact, CONTACT_FORM pour le formulaire intégré.
  • formCodeName / formName - identifient le formulaire personnalisé à l'origine de la demande ; tous deux valent null pour le formulaire intégré.
  • Lorsqu'un formulaire personnalisé est utilisé, chaque champ défini sur ce formulaire est inclus dans fields (même format {name, value, type} que lead.created). Avec le formulaire de contact intégré, seuls email et message sont renseignés, fields est un tableau vide et les identifiants de formulaire valent null.
  • message - le champ de message mappé, ou toutes les valeurs renseignées jointes entre elles si le formulaire ne définit aucun champ de message.

custom_form.submitted

Se déclenche pour chaque soumission de formulaire personnalisé, quel que soit l'objectif du formulaire. Notez que les formulaires dont l'objectif est la collecte de leads ou le contact humain déclenchent également leur événement dédié lead.created / contact_form.submitted - abonnez-vous à l'un ou à l'autre selon que vous souhaitez une vue générique ou spécialisée, au lieu de créer des actions métier en double. Les deux familles d'événements possèdent des identifiants d'événement différents ; la combinaison conversationId et timestamp ne constitue pas un identifiant de soumission fiable.

{
  "eventId": "6c1d8e3f-2a5b-4c7d-9e0f-1a4b6c8d0e2f",
  "eventType": "custom_form.submitted",
  "timestamp": "2026-08-13T14:27:45Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "formCodeName": "warranty_claim",
    "formName": "Warranty claim",
    "fields": [
      {"name": "order_number", "value": "A-10293", "type": "text"},
      {"name": "issue", "value": "Damaged on arrival", "type": "textarea"},
      {"name": "photo", "value": "https://api.chatlab.com/aichat/customform/download?key=...&token=...", "type": "file"}
    ],
    "purpose": "STANDALONE"
  }
}
  • formCodeName - le nom système stable du formulaire, inchangé lorsque vous renommez le formulaire ; utilisez-le pour router les soumissions dans votre propre système. formName correspond au libellé affiché aux visiteurs.
  • fields utilise les mêmes entrées {name, value, type} que lead.created : les valeurs à choix multiples sont des tableaux, les valeurs de fichiers sont des liens de téléchargement.
  • purpose - STANDALONE, LEAD_COLLECTION ou HUMAN_CONTACT, selon la manière dont le formulaire est relié au chatbot.

conversation.started

Se déclenche lorsqu'un visiteur envoie le premier message d'une nouvelle conversation.

{
  "eventId": "8e2f0a4b-3c6d-4e8f-a1b2-2c5d7e9f1a3b",
  "eventType": "conversation.started",
  "timestamp": "2026-08-13T14:20:11Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "firstMessage": "Do you ship to Canada?",
    "chatSource": "WIDGET",
    "byAdmin": false,
    "countryCode": "PL",
    "ipAddress": "83.12.44.7"
  }
}
  • firstMessage - le texte exact du message d'ouverture du visiteur. null si la conversation a été ouverte sans contenu de message.
  • chatSource - le canal par lequel la conversation est arrivée : WIDGET, WHATSAPP, MESSENGER, VOICE, VOICE_PHONE, API, BOOKING, AIRBNB ou IDOBOOKING.
  • byAdmin - true lorsque la conversation provient de l'aperçu du chatbot dans le panneau d'administration de ChatLab plutôt que d'un vrai visiteur. Utilisez-le pour exclure vos propres tests de votre CRM.
  • countryCode - code pays ISO déterminé à partir de l'adresse IP du visiteur, null lorsqu'il n'a pas pu être déterminé.
  • ipAddress - l'adresse IP du visiteur telle que vue par ChatLab, null si indisponible. Traitez-la comme une donnée personnelle au sens du RGPD et ne la conservez que si vous disposez d'une base légale.

conversation.rated

Se déclenche lorsqu'un visiteur attribue une note positive ou négative à la conversation (voir Évaluation de la conversation).

{
  "eventId": "1b4c6d8e-5f0a-4b2c-8d3e-4f7a9b1c3d5e",
  "eventType": "conversation.rated",
  "timestamp": "2026-08-13T14:31:09Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "rating": "POSITIVE"
  }
}
  • rating - POSITIVE ou NEGATIVE. L'effacement d'une note ne déclenche pas l'événement, vous ne recevez donc jamais de valeur neutre.

conversation.summarized

Se déclenche lorsque ChatLab génère le résumé d'une conversation terminée.

{
  "eventId": "4d7e9f1a-6b2c-4d4e-9f0a-5b8c0d2e4f6a",
  "eventType": "conversation.summarized",
  "timestamp": "2026-08-13T14:45:00Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "summary": "Visitor asked about shipping to Canada and delivery times. The bot confirmed availability and quoted 5-7 business days. Visitor left satisfied.",
    "language": "en_US"
  }
}
  • summary - le texte du résumé généré. La génération du résumé est asynchrone et dépend de la configuration du résumé du bot ainsi que du calendrier de traitement ; ne présumez pas d'un délai de livraison fixe.
  • language - la locale interne du bot transmise pour la synthèse, par exemple en_US ; ne partez pas du principe qu'elle correspond à la langue de conversation du visiteur.

client.summarized

Se déclenche lorsque ChatLab actualise le profil IA d'un client. Le profil est reconstruit à partir du profil précédent et du résumé de la conversation qui vient de se terminer, et peut être généré après conversation.summarized. L'ordre de distribution n'est pas garanti. La charge utile inclut une adresse e-mail lorsqu'elle est connue, mais ChatLab peut également conserver des clients sans e-mail.

{
  "eventId": "b5d8f1a3-7c2e-4d9b-a6f0-1e3c5a7b9d2f",
  "eventType": "client.summarized",
  "timestamp": "2026-08-18T09:12:04Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "clientEmail": "jane.doe@example.com",
    "client": {
      "email": "jane.doe@example.com",
      "name": "Jane Doe",
      "phone": "+1 555 0123",
      "countryCode": "PL",
      "ipAddress": "83.12.44.7"
    },
    "clientSummary": "Returning customer interested in international shipping. Asked about delivery times to Canada twice and about return costs once."
  }
}
  • clientEmail - l'identifiant permettant de faire correspondre le client avec votre propre CRM. Il vaut null pour les visiteurs anonymes qui n'ont jamais laissé d'adresse, et l'événement se déclenche quand même pour eux - ignorez ces envois si votre intégration est basée sur l'e-mail.
  • client - la fiche de contact conservée par ChatLab pour cette personne : email, name, phone, countryCode et ipAddress. Chaque clé est toujours présente ; les valeurs inconnues sont null.
  • clientSummary - le texte intégral du profil au format texte brut, pas un différentiel. Il remplace le résumé précédent, veillez donc à enregistrer la valeur par écrasement plutôt que par ajout.
  • Le profil n'est reconstruit que pour les bots dont la mémoire du chat est activée, et uniquement pour les conversations restées inactives assez longtemps pour être résumées - attendez-vous à recevoir cet événement quelques minutes après la fin de la conversation, et non immédiatement.

live_chat.requested

Se déclenche lorsque l'IA transfère la conversation au Live Chat (chat en direct), soit parce que le visiteur a demandé un conseiller humain, soit parce que le bot a jugé qu'une intervention humaine était nécessaire.

{
  "eventId": "7a0b2c4d-8e3f-4a5b-b0c1-6d9e1f3a5b7c",
  "eventType": "live_chat.requested",
  "timestamp": "2026-08-13T14:33:20Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "requestedBy": "AI"
  }
}
  • requestedBy - vaut actuellement toujours AI, car le transfert est toujours déclenché par l'action de chat en direct du bot, y compris lorsque le visiteur le demande en texte libre. Considérez-le comme une énumération ouverte : prévoyez la gestion de valeurs inconnues au lieu d'exiger impérativement AI.
  • Cet événement couvre l'action de l'IA qui sollicite un transfert, et non toutes les façons dont un visiteur peut ouvrir le chat en direct. Il ne garantit pas qu'un opérateur a rejoint la conversation.

live_chat.started

Se déclenche lorsque la session de chat en direct est créée après la soumission du formulaire de transfert par le visiteur. Cela se produit sans qu'un opérateur ait nécessairement rejoint la conversation ou répondu ; ne le considérez pas comme la preuve d'un échange humain effectif.

{
  "eventId": "0c3d5e7f-9a4b-4c6d-a1b2-7e0f2a4b6c8d",
  "eventType": "live_chat.started",
  "timestamp": "2026-08-13T14:33:55Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {}
}
  • data est intentionnellement vide. Tout ce dont vous avez besoin se trouve dans l'enveloppe : botId identifie le chatbot et conversationId / sessionId associent l'événement à la conversation. Vous pouvez le recevoir sans avoir reçu de live_chat.requested préalable, par exemple si le visiteur a utilisé la commande de chat en direct du widget.

live_chat.ended

Se déclenche lorsque la session de chat en direct prend fin.

{
  "eventId": "2e5f7a9b-0c5d-4e7f-b2c3-8f1a3b5c7d9e",
  "eventType": "live_chat.ended",
  "timestamp": "2026-08-13T14:52:41Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "durationSeconds": 1126
  }
}
  • durationSeconds - durée écoulée de la session de chat en direct, calculée à partir de la création de la session, temps d'attente d'un opérateur inclus. Le champ est omis dans les rares cas où une session se termine sans avoir jamais débuté.

ai_action.executed

Se déclenche chaque fois que le bot exécute une action d'IA - un appel d'intégration géré ou une fonction d'API personnalisée. Il s'agit d'un événement à fort volume : un bot e-commerce actif peut exécuter des centaines d'actions par jour, et un seul tour de conversation avec un visiteur peut en déclencher plusieurs. Abonnez-vous à cet événement sur un point de terminaison dédié, ou assurez-vous que votre serveur récepteur peut absorber un tel volume.

{
  "eventId": "5f8a0b2c-1d6e-4f8a-c3d4-9a2b4c6d8e0f",
  "eventType": "ai_action.executed",
  "timestamp": "2026-08-13T14:21:03Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "actionName": "search_products",
    "status": "SUCCESS",
    "durationMs": 842,
    "errorMessage": null
  }
}
  • actionName - le nom de l'action exécutée tel que vu par l'IA, par exemple search_products pour une intégration gérée ou le nom que vous avez donné à une action d'API personnalisée.
  • status - SUCCESS ou ERROR.
  • durationMs - le temps d'exécution de l'action, en millisecondes. Pratique pour repérer une intégration lente avant que les visiteurs ne s'en plaignent.
  • errorMessage - le motif de l'échec, renseigné uniquement lorsque status vaut ERROR ; null dans le cas contraire.

webhook.test

Envoyé par le bouton Send sample event (Envoyer un événement test) lorsque vous effectuez un simple test de connectivité. Signé exactement comme un événement réel.

{
  "eventId": "9b2c4d6e-3f8a-4b0c-d5e6-0b3c5d7e9f1a",
  "eventType": "webhook.test",
  "timestamp": "2026-08-13T14:10:00Z",
  "botId": 1234,
  "botName": "Support Bot",
  "conversationId": "conv_a1b2c3",
  "sessionId": "sess_x9y8z7",
  "data": {
    "message": "Test delivery from ChatLab"
  }
}
  • message - texte fixe, toujours identique. Les champs de l'enveloppe comportent des valeurs d'exemple, ne traitez donc jamais un envoi webhook.test comme des données réelles.
  • Il s'agit du seul type d'événement auquel vous ne pouvez pas vous abonner sur un point de terminaison : il est envoyé à la demande depuis le panneau d'administration et parvient toujours au point de terminaison sur lequel vous avez cliqué, quels que soient les événements qu'il écoute.

Sécurité : vérifier les distributions

Chaque distribution contient quatre en-têtes :

En-tête Valeur
X-ChatLab-Signature sha256=<hex hmac> - signature HMAC-SHA256 de la charge utile
X-ChatLab-Timestamp Heure Unix en secondes au moment où la distribution a été signée
X-ChatLab-Event Le type d'événement, par ex. lead.created
X-ChatLab-Delivery Identifiant unique de la distribution, identique au eventId du corps

La signature est calculée sous forme de HMAC-SHA256 sur la chaîne {timestamp}.{rawBody} à l'aide du secret de votre point de terminaison, où {timestamp} correspond à la valeur de X-ChatLab-Timestamp et {rawBody} au corps brut et non analysé de la requête. Vérifiez toujours par rapport aux octets bruts - resérialiser du JSON analysé modifiera la séquence d'octets et invalidera la signature.

Pour vous protéger contre les attaques par rejeu, rejetez les distributions dont le X-ChatLab-Timestamp date de plus de 5 minutes.

Node.js

const crypto = require('crypto');

function verifyChatLabSignature(req, secret) {
    const signature = req.headers['x-chatlab-signature'];
    const timestamp = req.headers['x-chatlab-timestamp'];
    if (typeof signature !== 'string' || typeof timestamp !== 'string') return false;
    if (!/^\d+$/.test(timestamp) || !Number.isSafeInteger(Number(timestamp))) return false;
    if (!Buffer.isBuffer(req.rawBody)) return false;

    // Reject stale deliveries (older than 5 minutes)
    const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
    if (ageSeconds > 300) return false;

    // rawBody must be the raw request body bytes, not re-serialized JSON.
    // With Express: app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }))
    const expected = 'sha256=' + crypto
        .createHmac('sha256', secret)
        .update(timestamp + '.')
        .update(req.rawBody)
        .digest('hex');

    const a = Buffer.from(signature);
    const b = Buffer.from(expected);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
}

PHP

<?php
function verifyChatLabSignature(string $secret): bool
{
    $signature = $_SERVER['HTTP_X_CHATLAB_SIGNATURE'] ?? '';
    $timestamp = $_SERVER['HTTP_X_CHATLAB_TIMESTAMP'] ?? '';
    if ($signature === '' || $timestamp === '' || !ctype_digit($timestamp)) {
        return false;
    }

    // Reject stale deliveries (older than 5 minutes)
    if (abs(time() - (int) $timestamp) > 300) {
        return false;
    }

    $rawBody = file_get_contents('php://input');
    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

    return hash_equals($expected, $signature);
}

Si la vérification échoue, répondez avec 401 et ignorez la charge utile. Ne traitez jamais les distributions non vérifiées - toute personne découvrant votre URL peut y envoyer du JSON arbitraire via une requête POST.

Comportement de distribution

Prenez connaissance de ces règles de distribution avant d'utiliser les webhooks :

  • Répondez rapidement. Votre point de terminaison doit répondre en moins de 3 secondes, sans quoi la distribution est considérée comme ayant échoué. Vérifiez la signature, mettez l'événement en file d'attente de manière durable et confirmez la réception avec 2xx dans ce délai. Effectuez les appels CRM et les traitements métier plus lents de manière asynchrone.
  • Envoi avec au plus une tentative (Fire-and-forget, at-most-once). Les points de terminaison activés correspondants reçoivent au maximum une tentative de distribution - il n'y a pas de nouvelle tentative. Les points de terminaison en pause et les disjoncteurs ouverts peuvent même bloquer cette tentative. Si votre point de terminaison est indisponible, expire ou renvoie un statut non-2xx, cet événement est perdu et ne sera pas renvoyé. Les webhooks sont des notifications, pas un magasin de données répliqué : utilisez les points de terminaison de conversation de la Bot Talk API ou les exports de leads pour rapprocher les données conservées lorsque cela est possible. La Management API couvre les paramètres et l'utilisation du bot, pas une archive complète des événements. Certains événements ne peuvent pas être reconstruits par le biais de ces interfaces.
  • Disjoncteur (Circuit breaker). Après 5 distributions consécutives en échec pour une paire point de terminaison/bot, les distributions pour cette paire sont suspendues pendant 5 minutes. Les événements survenant pendant cette pause sont abandonnés, et le journal de distribution affiche des entrées CIRCUIT_OPEN pour identifier les tentatives supprimées. Les distributions ignorées en raison d'un disjoncteur ouvert ne sont pas prises en compte dans la désactivation automatique.
  • Désactivation automatique. La vérification s'exécute au moment où une distribution échoue, jamais selon un calendrier programmé. Si une distribution échoue et qu'il n'y a eu aucune distribution réussie pendant 7 jours - calculés à partir du dernier succès, ou de la date de création du point de terminaison s'il n'a encore jamais abouti - le point de terminaison est désactivé et vous recevez une notification par e-mail. Un seul 2xx à tout moment réinitialise ce compteur. Un point de terminaison qui ne reçoit aucun trafic n'est jamais désactivé, car rien n'échoue. Réactivez-le depuis les paramètres du compte (Account settings) une fois votre récepteur réparé ; le compteur d'échecs et l'horodatage de désactivation automatique sont réinitialisés lorsque vous le réactivez, et les événements manqués pendant qu'il était désactivé ne sont pas récupérés rétroactivement.
  • 410 Gone. Si votre point de terminaison répond avec le code HTTP 410 Gone, ChatLab le désactive immédiatement. Utilisez cette méthode pour mettre un point de terminaison hors service par programmation depuis le système récepteur.
  • Idempotence. Les distributions en double ne sont pas attendues en fonctionnement normal, mais si votre traitement doit être strictement idempotent, dédupliquez à l'aide de eventId (également disponible dans l'en-tête X-ChatLab-Delivery).

L'ordre de distribution n'est pas garanti. Enregistrez l'identifiant d'événement et rendez le traitement métier idempotent. Définissez vos propres règles de conservation et de contrôle d'accès pour les charges utiles de webhook, qui peuvent contenir des données personnelles et des liens de téléchargement de fichiers.

Les exemples de signature utilisent Node.js crypto et la fonction hash_equals de PHP. Récupérez le corps brut de la requête (raw request body) avant de l'analyser.

Limites

  • Jusqu'à 10 points de terminaison de webhook par compte.
  • Rétention du journal de distribution : 14 jours. Les entrées plus anciennes sont supprimées automatiquement.

Articles connexes