Helpcentrum
Chat API

Webhooks

Laatst bijgewerkt:

Webhooks-overzicht

Met webhooks kan ChatLab jouw systemen direct op de hoogte stellen zodra er iets gebeurt in jouw chatbots. In plaats van het pollen van de Management API of het handmatig exporteren van gegevens, registreer je een HTTPS-eindpunt en stuurt ChatLab hier in realtime een ondertekend HTTP POST-verzoek naartoe - wanneer een bezoeker een lead achterlaat, een contactformulier verzendt, een gesprek beoordeelt, om een menselijke medewerker vraagt, of wanneer een AI-actie wordt uitgevoerd.

Typische toepassingen:

  • nieuwe leads direct naar jouw CRM doorsturen op het moment dat ze worden vastgelegd
  • jouw team waarschuwen in Slack wanneer een bezoeker om Live Chat (livechat) vraagt
  • gespreksbeoordelingen en samenvattingen doorsturen naar jouw eigen analytics
  • de uitvoering van AI-acties monitoren en waarschuwingen sturen bij fouten

Beschikbaarheid: jouw account moet beschikken over de functie Webhooks.

Waar te configureren: open in de beheeromgeving Account settings -> Webhooks (Accountinstellingen -> Webhooks; direct naast het onderdeel Management API). Webhooks werken op accountniveau - één eindpunt kan gebeurtenissen ontvangen van al jouw bots of van één geselecteerde bot.

Een eindpunt instellen

  1. Open Account settings -> Webhooks en klik op Add endpoint (Eindpunt toevoegen).
  2. Vul het eindpuntformulier in:
    • Name (Naam) - een label voor eigen referentie, bijv. "CRM-synchronisatie" of "Slack-meldingen".
    • URL - het HTTPS-adres waar ChatLab de gebeurtenissen naartoe stuurt via een POST-verzoek.
    • Events (Gebeurtenissen) - selecteer welke gebeurtenistypen dit eindpunt ontvangt (zie het onderstaande overzicht). Selecteer alleen wat je nodig hebt; gebeurtenissen met een hoog volume zoals ai_action.executed kunnen veel dataverkeer genereren.
    • Bot filter (Botfilter) (optioneel) - selecteer één bot, of All bots (Alle bots) voor alle bots binnen jouw account.
    • Custom form filter (Aangepast formulierfilter) (optioneel) - stuurt inzendingen van één aangepast formulier door naar dit eindpunt. Dit filtert uitsluitend de gebeurtenis custom_form.submitted; elke andere gebeurtenis waarop je bent geabonneerd (leads, contactaanvragen, gesprekken, Live Chat, AI-acties) wordt ongeacht deze instelling afgeleverd.
  3. Klik op opslaan/verzenden. Het secret (geheim) van het eindpunt wordt precies één keer getoond in het bevestigingsvenster - kopieer het direct en bewaar het veilig. Je hebt het nodig om handtekeningen te verifiëren (zie Beveiliging hieronder). De tekst zonder opmaak kan later niet meer worden opgehaald.

Elk eindpunt beschikt ook over:

  • Schakelaar voor in-/uitschakelen - pauzeer leveringen zonder het eindpunt te verwijderen. Uitgeschakelde eindpunten negeren gebeurtenissen geruisloos (ze worden niet in een wachtrij geplaatst voor later).
  • Send sample event (Voorbeeldgebeurtenis verzenden) - stuurt een ondertekend testverzoek naar jouw URL, zodat je jouw ontvanger van begin tot eind kunt controleren. Je kunt het gebeurtenistype kiezen en de voorbeeldwaarden bewerken voordat je verzendt, zodat jouw verwerkingssysteem realistische gegevens te zien krijgt. De test arriveert als een reguliere levering met een eventType dat overeenkomt met jouw selectie (of als webhook.test voor een eenvoudige connectiviteitscontrole).
  • Roll secret (Geheim vernieuwen) - genereert een nieuw geheim en maakt het oude ongeldig. Gebruik dit als het geheim mogelijk is gelekt. Het nieuwe geheim wordt opnieuw slechts één keer getoond. Voor een gecontroleerde rotatie pauzeer je het eindpunt, vernieuw en kopieer je het nieuwe geheim, werk je de ontvanger bij, schakel je het eindpunt weer in en verstuur je een voorbeeldgebeurtenis. Gebeurtenissen tijdens de pauze worden niet in een wachtrij geplaatst. Houd rekening met deze onderbreking voordat je gaat roteren.
  • Delivery log (Leveringslogboek) - een lijst per eindpunt met recente leveringen inclusief tijdstempel, gebeurtenistype, door jouw server geretourneerde HTTP-status en responstijd. Mislukte leveringen en onderbrekingen door de circuit breaker zijn hier zichtbaar. Het logboek wordt 14 dagen bewaard.

Gebeurtenis-envelope

Elke levering is een HTTP POST met Content-Type: application/json. De hoofdtekst (body) heeft altijd dezelfde envelope; het data-object is specifiek voor het type gebeurtenis:

{
  "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 - uniek per gebeurtenis. Gebruik dit voor ontdubbeling (deduplicatie) als jouw verwerking idempotent moet zijn.
  • eventType - een van de hieronder gedocumenteerde typen; wordt ook verzonden in de header X-ChatLab-Event.
  • timestamp - het tijdstip in ISO 8601 UTC waarop de delivery-envelope is voorbereid.
  • botId / botName - de bot waartoe de gebeurtenis behoort.
  • conversationId / sessionId - de gesprekscontext, indien van toepassing.

Gebeurteniscatalogus

lead.created

Wordt geactiveerd wanneer een bezoeker zijn contactgegevens verzendt - via het formulier voor leadverzameling, het livechat-voorformulier of een aangepast formulier dat wordt gebruikt voor leadverzameling.

{
  "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 - hoe de contactgegevens zijn verzameld: LEAD_COLLECTION_FORM (formulier voor leadverzameling), LIVE_CHAT_FORM (livechat-voorformulier), CONVERSATION (de AI heeft de gegevens tijdens het gesprek opgevangen), ADMIN_DATA_UPDATE of UPDATE_CLIENT_CONTEXT (bewerkt aan de ChatLab-zijde). Inzendingen van het formulier voor menselijke ondersteuning activeren deze gebeurtenis nooit - zij activeren in plaats daarvan contact_form.submitted.
  • email, name, phone - de contactgegevens die zijn gekoppeld aan het leadrecord.
  • Wanneer een aangepast formulier wordt gebruikt voor leadverzameling, wordt elk veld dat op dat formulier is gedefinieerd opgenomen in fields, in de volgorde van het formulier, en identificeren formCodeName / formName het formulier. Bij het klassieke leadformulier zijn beide null en is fields een lege array.
  • Elk item in fields is {name, value, type}. name is de technische naam van het veld, die gelijk blijft bij wijzigingen van labels - gebruik deze voor de koppeling met je CRM.
  • Voor multichoice-velden is value een array van de geselecteerde opties. Selectievakjes zijn afzonderlijke items met de waarden "true" / "false".
  • Voor file-velden is value een downloadlink naar het geüploade bestand; de webhook bevat nooit de bestandsinhoud zelf.
  • pageUrl - de pagina waarop de bezoeker zich bevond op het moment van verzenden.

contact_form.submitted

Wordt geactiveerd wanneer een bezoeker het contactformulier voor menselijke ondersteuning verzendt of een aangepast formulier dat wordt gebruikt voor menselijk contact.

{
  "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 - het adres dat de bezoeker heeft achtergelaten en waarnaar je supportteam moet antwoorden.
  • source - CUSTOM_FORM wanneer een aangepast formulier als contactformulier dient, CONTACT_FORM voor het ingebouwde formulier.
  • formCodeName / formName - identificeren het aangepaste formulier achter het verzoek; beide zijn null voor het ingebouwde formulier.
  • Wanneer een aangepast formulier wordt gebruikt, wordt elk veld dat op dat formulier is gedefinieerd opgenomen in fields (dezelfde {name, value, type}-indeling als bij lead.created). Bij het ingebouwde contactformulier worden alleen email en message gevuld, is fields een lege array en zijn de formulier-id's null.
  • message - het gekoppelde berichtveld, of alle ingevulde waarden samengevoegd wanneer het formulier geen berichtveld definieert.

custom_form.submitted

Wordt geactiveerd voor elke inzending van een aangepast formulier, ongeacht het doel van het formulier. Let op: formulieren die bedoeld zijn voor leadverzameling of menselijk contact activeren ook hun eigen specifieke gebeurtenis lead.created / contact_form.submitted - abonneer je op de ene of de andere, afhankelijk van of je de generieke of de gespecialiseerde weergave wilt, om dubbele bedrijfsprocessen te voorkomen. De twee gebeurtenisfamilies hebben verschillende event-ID's; conversationId samen met timestamp is geen betrouwbare identificator voor inzendingen.

{
  "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 - de vaste technische naam van het formulier, die ongewijzigd blijft wanneer je de naam van het formulier aanpast; gebruik deze om inzendingen in je eigen systeem te routeren. formName is het weergavelabel dat aan bezoekers wordt getoond.
  • fields gebruikt dezelfde {name, value, type}-items als lead.created: meerkeuzewaarden zijn arrays, bestandswaarden zijn downloadlinks.
  • purpose - STANDALONE, LEAD_COLLECTION of HUMAN_CONTACT, afhankelijk van hoe het formulier aan de chatbot is gekoppeld.

conversation.started

Wordt geactiveerd wanneer een bezoeker het eerste bericht van een nieuw gesprek verzendt.

{
  "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 - de exacte tekst van het openingsbericht van de bezoeker. null als het gesprek werd geopend zonder berichtinhoud.
  • chatSource - het kanaal waarover het gesprek binnenkwam: WIDGET, WHATSAPP, MESSENGER, VOICE, VOICE_PHONE, API, BOOKING, AIRBNB of IDOBOOKING.
  • byAdmin - true wanneer het gesprek afkomstig is van het chatbotvoorbeeld in het ChatLab-beheerderspaneel en niet van een echte bezoeker. Gebruik dit om je eigen testgesprekken buiten je CRM te houden.
  • countryCode - ISO-landcode bepaald aan de hand van het IP-adres van de bezoeker, null wanneer deze niet kon worden vastgesteld.
  • ipAddress - het IP-adres van de bezoeker zoals gezien door ChatLab, null wanneer niet beschikbaar. Behandel dit als persoonsgegevens onder de AVG en sla het alleen op als je daarvoor een wettelijke grondslag hebt.

conversation.rated

Wordt geactiveerd wanneer een bezoeker een positieve of negatieve gespreksbeoordeling achterlaat (zie Gespreksbeoordeling).

{
  "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 of NEGATIVE. Het wissen van een beoordeling activeert de gebeurtenis niet, dus je ontvangt nooit een neutrale waarde.

conversation.summarized

Wordt geactiveerd wanneer ChatLab een samenvatting genereert van een afgerond gesprek.

{
  "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 - de gegenereerde samenvattingstekst. Het genereren van de samenvatting gebeurt asynchroon en is afhankelijk van de samenvattingsconfiguratie en de verwerkingsplanning van de bot; ga niet uit van een vaste vertragingstijd.
  • language - de interne locale van de bot die wordt doorgegeven aan de samenvatting, bijvoorbeeld en_US; ga er niet vanuit dat dit overeenkomt met de gesprekstaal van de bezoeker.

client.summarized

Wordt geactiveerd wanneer ChatLab het AI-profiel van een klant bijwerkt. Het profiel wordt opnieuw opgebouwd op basis van het vorige profiel plus de samenvatting van het zojuist beëindigde gesprek, en kan worden gegenereerd na conversation.summarized. De aflevervolgorde is niet gegarandeerd. De payload bevat een e-mailadres wanneer dit bekend is, maar ChatLab kan ook klanten zonder e-mailadres bevatten.

{
  "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 - de identificator om de klant te koppelen aan je eigen CRM. Deze is null voor anonieme bezoekers die nooit een adres hebben achtergelaten, en de gebeurtenis wordt ook voor hen geactiveerd - sla deze afleveringen over als je integratie is gebaseerd op e-mail.
  • client - het contactrecord dat ChatLab voor deze persoon bijhoudt: email, name, phone, countryCode en ipAddress. Elke sleutel is altijd aanwezig; onbekende waarden zijn null.
  • clientSummary - de volledige profieltekst als platte tekst, geen diff. Deze vervangt de vorige samenvatting volledig, dus sla deze op door de oude te overschrijven in plaats van toe te voegen.
  • Het profiel wordt alleen opnieuw opgebouwd voor bots met chatgeheugen ingeschakeld, en alleen voor gesprekken die lang genoeg inactief waren om te worden samengevat - verwacht deze gebeurtenis enkele minuten nadat het gesprek is beëindigd, niet onmiddellijk.

live_chat.requested

Wordt geactiveerd wanneer de AI het gesprek overdraagt aan Live Chat (livechat), hetzij omdat de bezoeker om een menselijke medewerker vroeg, hetzij omdat de bot besloot dat een menselijke medewerker nodig was.

{
  "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 - momenteel altijd AI, omdat de overdracht altijd wordt gestart door de livechat-actie van de bot, ook wanneer de bezoeker er in duidelijke bewoordingen om vraagt. Behandel dit als een open enum: vang onbekende waarden op in plaats van strikt op AI te controleren.
  • Deze gebeurtenis dekt de AI-actie die om overdracht vraagt, niet alle manieren waarop een bezoeker een livechat kan openen. Het is geen bewijs dat een medewerker daadwerkelijk heeft deelgenomen.

live_chat.started

Wordt geactiveerd wanneer de livechat-sessie wordt aangemaakt nadat het overdrachtsformulier van de bezoeker is verzonden. Dit gebeurt voordat een medewerker noodzakelijkerwijs deelneemt of antwoordt; beschouw dit niet als bewijs van menselijke interactie.

{
  "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 is opzettelijk leeg. Alles wat je nodig hebt bevindt zich in de envelop: botId identificeert de chatbot en conversationId / sessionId koppelen de gebeurtenis aan het gesprek. Je kunt deze gebeurtenis ontvangen zonder eerdere live_chat.requested, bijvoorbeeld wanneer de bezoeker de livechat-knop in de widget heeft gebruikt.

live_chat.ended

Wordt geactiveerd wanneer de livechat-sessie wordt beëindigd.

{
  "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 - verstreken duur van de livechat-sessie, geteld vanaf het aanmaken van de sessie, inclusief de wachttijd op een medewerker. Het veld wordt weggelaten in het zeldzame geval dat een sessie wordt beëindigd zonder ooit te zijn gestart.

ai_action.executed

Wordt elke keer geactiveerd wanneer de bot een AI-actie uitvoert - een beheerde integratie-aanroep of een aangepaste API-functie. Dit is een gebeurtenis met een hoog volume: een actieve e-commercechatbot kan honderden acties per dag uitvoeren, en een enkele interactie met een bezoeker kan er meerdere activeren. Abonneer je hierop via een specifiek eindpunt, of zorg ervoor dat je ontvanger het volume aankan.

{
  "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 - de naam van de uitgevoerde actie zoals de AI deze ziet, bijvoorbeeld search_products voor een beheerde integratie of de naam die je aan een aangepaste API-actie hebt gegeven.
  • status - SUCCESS of ERROR.
  • durationMs - hoe lang de actie duurde, in milliseconden. Handig om een trage integratie op te merken voordat bezoekers erover klagen.
  • errorMessage - de reden van de fout, alleen ingevuld wanneer status gelijk is aan ERROR; anders null.

webhook.test

Verzonden via de knop Send sample event (Voorbeeldgebeurtenis verzenden) wanneer je een eenvoudige connectiviteitscontrole uitvoert. Wordt op exact dezelfde manier ondertekend als een echte gebeurtenis.

{
  "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 - vaste tekst, altijd hetzelfde. De envelopvelden bevatten voorbeeldwaarden, dus behandel een webhook.test-aflevering nooit als echte gegevens.
  • Dit is het enige gebeurtenistype waarop je je niet kunt abonneren op een eindpunt: het wordt op verzoek verzonden vanuit het beheerderspaneel en bereikt altijd het eindpunt waarop je hebt geklikt, ongeacht naar welke gebeurtenissen het luistert.

Beveiliging: afleveringen verifiëren

Elke aflevering bevat vier headers:

Header Waarde
X-ChatLab-Signature sha256=<hex hmac> - HMAC-SHA256-handtekening van de payload
X-ChatLab-Timestamp Unix-tijd in seconden waarop de aflevering is ondertekend
X-ChatLab-Event Het gebeurtenistype, bijv. lead.created
X-ChatLab-Delivery Unieke afleverings-ID, gelijk aan eventId in de body

De handtekening wordt berekend als HMAC-SHA256 over de string {timestamp}.{rawBody} met gebruik van je eindpuntgeheim, waarbij {timestamp} de waarde is van X-ChatLab-Timestamp en {rawBody} de ruwe, niet-geparseerde request body is. Verifieer altijd aan de hand van de ruwe bytes - het opnieuw serialiseren van geparseerde JSON verandert de bytesequentie en maakt de handtekening ongeldig.

Om je te beschermen tegen replay-aanvallen, moet je afleveringen weigeren waarvan X-ChatLab-Timestamp ouder is dan 5 minuten.

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

Als de verificatie mislukt, antwoord dan met 401 en verwerp de payload. Verwerk nooit niet-geverifieerde leveringen - iedereen die je URL ontdekt kan willekeurige JSON ernaartoe posten via POST.

Aflevergedrag

Zorg dat je deze regels voor de aflevering begrijpt voordat je met webhooks aan de slag gaat:

  • Reageer snel. Jouw endpoint moet binnen 3 seconden antwoorden, anders geldt de aflevering als mislukt. Verifieer de handtekening, plaats het event veilig in een wachtrij en bevestig binnen dat tijdsvenster met een 2xx-statuscode. Voer tragere CRM-aanroepen en bedrijfslogica asynchroon uit.
  • Fire-and-forget, hoogstens één keer (at-most-once). Overeenkomende, ingeschakelde endpoints krijgen hoogstens één afleverpoging - er zijn geen nieuwe pogingen (retries). Gepauzeerde endpoints en actieve circuit breakers kunnen zelfs die ene poging tegenhouden. Als jouw endpoint offline is, een time-out geeft of een niet-2xx-statuscode retourneert, is dat event verloren en wordt het niet opnieuw verzonden. Webhooks zijn notificaties, geen gerepliceerde gegevensopslag: gebruik gesprek-endpoints van de Bot Talk API of lead-exports om opgeslagen gegevens te synchroniseren waar dat wordt ondersteund. De Management API omvat botinstellingen en gebruiksgegevens, maar geen compleet eventarchief. Sommige events kunnen via deze interfaces niet worden gereconstrueerd.
  • Circuit breaker. Na 5 opeenvolgende mislukte afleveringen voor een specifiek endpoint/bot-paar worden afleveringen voor dat paar gedurende 5 minuten gepauzeerd. Events die tijdens deze pauze plaatsvinden, worden genegeerd en in het afleverlogboek verschijnen CIRCUIT_OPEN-vermeldingen om tegengehouden pogingen aan te duiden. Afleveringen die door een geopende circuit breaker zijn overgeslagen, tellen niet mee voor het automatisch uitschakelen.
  • Automatisch uitschakelen. Deze controle wordt uitgevoerd op het moment dat een aflevering mislukt, nooit via een timer. Als een aflevering mislukt en er al 7 dagen geen succesvolle aflevering is geweest - gerekend vanaf het laatste succes, of vanaf de aanmaakdatum van het endpoint als er nog nooit een succesvolle aflevering was - wordt het endpoint uitgeschakeld en ontvang je een e-mailnotificatie. Eén enkele 2xx-statuscode op willekeurig welk moment reset deze timer. Een endpoint dat geen verkeer ontvangt, wordt nooit uitgeschakeld, omdat er niets mislukt. Schakel het weer in via Account settings (Accountinstellingen) zodra jouw ontvanger is hersteld; de foutenteller en het tijdstempel voor automatische uitschakeling worden gewist wanneer je het weer aanzet, en events die zijn gemist terwijl het uitstond, worden niet met terugwerkende kracht aangevuld.
  • 410 Gone. Als jouw endpoint antwoordt met HTTP 410 Gone, schakelt ChatLab dit direct uit. Gebruik dit om een endpoint programmatisch buiten gebruik te stellen vanaf de ontvangende kant.
  • Idempotentie. Dubbele afleveringen worden bij een normale werking niet verwacht, maar als jouw verwerking strikt idempotent moet zijn, ontdubbel dan op basis van de eventId (ook beschikbaar in de X-ChatLab-Delivery-header).

De aflevervolgorde is niet gegarandeerd. Sla de event-ID op en zorg dat de verwerking van de bedrijfslogica idempotent is. Hanteer jouw eigen bewaartermijnen en toegangscontroles voor webhook-payloads, aangezien deze persoonsgegevens en downloadlinks voor bestanden kunnen bevatten.

In de voorbeelden voor handtekeningverificatie wordt gebruikgemaakt van Node.js crypto en hash_equals van PHP. Vang de ruwe request-body op voordat je deze parsed.

Limieten

  • Maximaal 10 webhook-endpoints per account.
  • Bewaartermijn afleverlogboek: 14 dagen. Oudere vermeldingen worden automatisch verwijderd.

Gerelateerde artikelen