Centrum Pomocy
Chat API

Webhooki

Ostatnia aktualizacja:

Przegląd webhooków

Webhooki pozwalają ChatLab powiadamiać Twoje systemy w momencie, gdy coś wydarzy się w Twoich chatbotach. Zamiast odpytywać Management API lub ręcznie eksportować dane, rejestrujesz punkt końcowy HTTPS, a ChatLab wysyła do niego podpisane żądanie HTTP POST w czasie rzeczywistym - gdy odwiedzający zostawi leada, prześle formularz kontaktowy, oceni rozmowę, poprosi o kontakt z człowiekiem lub gdy wykona się akcja AI.

Typowe zastosowania:

  • przesyłanie nowych leadów bezpośrednio do Twojego systemu CRM w sekundzie ich pozyskania
  • powiadamianie Twojego zespołu na Slacku, gdy odwiedzający poprosi o czat na żywo
  • przekazywanie ocen i podsumowań rozmów do Twoich własnych narzędzi analitycznych
  • monitorowanie wykonań akcji AI i alertowanie o błędach

Dostępność: Twoje konto musi obejmować funkcję Webhooks.

Gdzie skonfigurować: w aplikacji administracyjnej otwórz Ustawienia konta (Account settings) -> Webhooks (tuż obok sekcji Management API). Webhooki działają na poziomie konta - jeden punkt końcowy może odbierać zdarzenia ze wszystkich Twoich botów lub z jednego wybranego bota.

Konfiguracja punktu końcowego (endpointu)

  1. Otwórz Account settings -> Webhooks (Ustawienia konta -> Webhooki) i kliknij Add endpoint (Dodaj punkt końcowy).
  2. Wypełnij formularz punktu końcowego:
    • Name (Nazwa) - etykieta dla Twojej własnej wygody, np. "Synchronizacja CRM" lub "Powiadomienia Slack".
    • URL - adres HTTPS, pod który ChatLab będzie wysyłać zdarzenia metodą POST.
    • Events (Zdarzenia) - wybierz typy zdarzeń, które ten endpoint ma odbierać (zobacz katalog poniżej). Wybieraj tylko to, czego potrzebujesz; zdarzenia o dużym natężeniu, takie jak ai_action.executed, mogą generować znaczny ruch.
    • Bot filter (Filtr bota) (opcjonalnie) - wybierz jednego bota lub All bots (Wszystkie boty), aby uwzględnić wszystkie boty na Twoim koncie.
    • Custom form filter (Filtr formularza niestandardowego) (opcjonalnie) - kieruje zgłoszenia z jednego formularza niestandardowego do tego punktu końcowego. Zawęża to wyłącznie zdarzenie custom_form.submitted; wszystkie inne subskrybowane zdarzenia (leady, prośby o kontakt, rozmowy, czat na żywo, akcje AI) są dostarczane bez względu na to ustawienie.
  3. Zapisz formularz. Klucz tajny (secret) punktu końcowego jest wyświetlany dokładnie raz w oknie potwierdzenia - skopiuj go od razu i przechowuj w bezpiecznym miejscu. Będziesz go potrzebować do weryfikacji podpisów (zobacz sekcję Bezpieczeństwo poniżej). Nie ma możliwości późniejszego odzyskania go w postaci jawnego tekstu.

Każdy punkt końcowy zawiera również:

  • Przełącznik włączania/wyłączania - wstrzymuje dostarczanie zdarzeń bez usuwania punktu końcowego. Wyłączone punkty końcowe po cichu odrzucają zdarzenia (nie są one kolejkowane na później).
  • Send sample event (Wyślij zdarzenie testowe) - wysyła podpisane żądanie testowe pod Twój adres URL, dzięki czemu możesz kompleksowo zweryfikować działanie odbiornika. Możesz wybrać typ zdarzenia i edytować przykładowe wartości przed wysłaniem, aby Twój skrypt obsługujący otrzymał realistyczne dane. Test dociera jako zwykłe doręczenie z polem eventType odpowiadającym Twojemu wyborowi (lub jako webhook.test w przypadku zwykłego sprawdzenia łączności).
  • Roll secret (Wygeneruj nowy klucz tajny) - generuje nowy klucz tajny i unieważnia poprzedni. Użyj tej opcji, jeśli istnieje ryzyko wycieku klucza. Nowy klucz tajny zostanie ponownie wyświetlony tylko raz. Aby przeprowadzić bezpieczną rotację, wstrzymaj punkt końcowy, wygeneruj i skopiuj nowy klucz, zaktualizuj swój odbiornik, a następnie włącz punkt końcowy z powrotem i wyślij zdarzenie testowe. Zdarzenia z okresu wstrzymania nie są kolejkowane. Zaplanuj tę przerwę przed przystąpieniem do rotacji.
  • Delivery log (Dziennik doręczeń) - lista ostatnich doręczeń dla danego punktu końcowego ze znacznikiem czasu, typem zdarzenia, statusem HTTP zwróconym przez Twój serwer oraz czasem odpowiedzi. Widoczne są tutaj nieudane doręczenia oraz wstrzymania przez mechanizm circuit breaker. Dziennik jest przechowywany przez 14 dni.

Koperta zdarzenia

Każde doręczenie to żądanie HTTP POST z Content-Type: application/json. Treść (body) zawsze zawiera tę samą kopertę; obiekt data zależy od typu zdarzenia:

{
  "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 - unikalny dla każdego zdarzenia. Użyj go do deduplikacji, jeśli Twoje przetwarzanie musi być idempotentne.
  • eventType - jeden z typów opisanych poniżej; przesyłany również w nagłówku X-ChatLab-Event.
  • timestamp - czas UTC w formacie ISO 8601 przygotowania koperty doręczenia.
  • botId / botName - bot, do którego należy zdarzenie.
  • conversationId / sessionId - kontekst rozmowy, jeśli dotyczy.

Katalog zdarzeń

lead.created

Uruchamia się, gdy odwiedzający przesyła swoje dane kontaktowe - za pośrednictwem formularza zbierania leadów, formularza wstępnego Live Chat lub formularza niestandardowego używanego do zbierania leadów.

{
  "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 - sposób pozyskania danych kontaktowych: LEAD_COLLECTION_FORM (formularz zbierania leadów), LIVE_CHAT_FORM (formularz wstępny Live Chat), CONVERSATION (sztuczna inteligencja wychwyciła dane podczas rozmowy), ADMIN_DATA_UPDATE lub UPDATE_CLIENT_CONTEXT (edytowane po stronie ChatLab). Przesłanie formularza kontaktu z człowiekiem nigdy nie wywołuje tego zdarzenia - zamiast tego wywołuje contact_form.submitted.
  • email, name, phone - dane kontaktowe zmapowane na rekord leada.
  • Gdy do zbierania leadów używany jest formularz niestandardowy, każde zdefiniowane w nim pole jest uwzględniane w fields, w kolejności z formularza, a formCodeName / formName identyfikują formularz. W przypadku klasycznego formularza leada oba mają wartość null, a fields jest pustą tablicą.
  • Każdy wpis w fields ma postać {name, value, type}. name to techniczna nazwa pola, niezmienna przy edycji etykiet - użyj jej do mapowania w swoim systemie CRM.
  • Dla pól multichoice wartość value jest tablicą wybranych opcji. Pola wyboru (checkbox) to pojedyncze wpisy z wartościami "true" / "false".
  • Dla pól file wartość value jest linkiem do pobrania przesłanego pliku; webhook nigdy nie przesyła zawartości plików.
  • pageUrl - strona, na której znajdował się odwiedzający w momencie wysłania formularza.

contact_form.submitted

Uruchamia się, gdy odwiedzający przesyła formularz kontaktu z obsługą lub formularz niestandardowy używany do kontaktu z człowiekiem.

{
  "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 - adres pozostawiony przez odwiedzającego, na który Twój zespół wsparcia powinien odpowiedzieć.
  • source - CUSTOM_FORM, gdy formularz kontaktowy jest oparty na formularzu niestandardowym, CONTACT_FORM dla formularza wbudowanego.
  • formCodeName / formName - identyfikują formularz niestandardowy stojący za zgłoszeniem; oba mają wartość null dla formularza wbudowanego.
  • Gdy używany jest formularz niestandardowy, uwzględniane jest każde zdefiniowane w nim pole w fields (taki sam format {name, value, type} jak w lead.created). W przypadku wbudowanego formularza kontaktowego wypełniane są tylko email i message, fields jest pustą tablicą, a identyfikatory formularza mają wartość null.
  • message - zmapowane pole wiadomości lub wszystkie wypełnione wartości połączone ze sobą, gdy formularz nie definiuje dedykowanego pola wiadomości.

custom_form.submitted

Uruchamia się przy każdym przesłaniu formularza niestandardowego, niezależnie od jego przeznaczenia. Pamiętaj, że formularze, których celem jest zbieranie leadów lub kontakt z człowiekiem, wywołują również swoje dedykowane zdarzenie lead.created / contact_form.submitted - zasubskrybuj jedno z nich w zależności od tego, czy potrzebujesz widoku ogólnego, czy wyspecjalizowanego, zamiast tworzyć zduplikowane akcje biznesowe. Obie rodziny zdarzeń mają różne identyfikatory zdarzeń; conversationId wraz z timestamp nie stanowi wiarygodnego identyfikatora zgłoszenia.

{
  "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 - stała nazwa techniczna formularza, która nie ulega zmianie po zmianie jego nazwy; użyj jej do kierowania zgłoszeń we własnym systemie. formName to etykieta wyświetlana odwiedzającym.
  • fields używa tych samych wpisów {name, value, type} co lead.created: wartości wielokrotnego wyboru są tablicami, a wartości plików to linki do pobrania.
  • purpose - STANDALONE, LEAD_COLLECTION lub HUMAN_CONTACT, w zależności od sposobu podpięcia formularza pod chatbota.

conversation.started

Uruchamia się, gdy odwiedzający wyśle pierwszą wiadomość w nowej rozmowie.

{
  "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 - dokładna treść pierwszej wiadomości odwiedzającego. null, jeśli rozmowa została otwarta bez treści wiadomości.
  • chatSource - kanał, z którego nadeszła rozmowa: WIDGET, WHATSAPP, MESSENGER, VOICE, VOICE_PHONE, API, BOOKING, AIRBNB lub IDOBOOKING.
  • byAdmin - true, gdy rozmowa pochodzi z podglądu chatbota wewnątrz panelu administracyjnego ChatLab, a nie od prawdziwego odwiedzającego. Użyj tej wartości, aby odsiać własne czaty testowe ze swojego CRM-a.
  • countryCode - kod kraju ISO ustalony na podstawie adresu IP odwiedzającego, null, gdy nie udało się go określić.
  • ipAddress - adres IP odwiedzającego widziany przez ChatLab, null, gdy jest niedostępny. Traktuj go jako dane osobowe w rozumieniu RODO i przechowuj tylko wtedy, gdy masz do tego podstawę prawną.

conversation.rated

Uruchamia się, gdy odwiedzający prześle pozytywną lub negatywną ocenę rozmowy (zobacz Ocenianie rozmów).

{
  "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 lub NEGATIVE. Usunięcie oceny nie wywołuje zdarzenia, więc nigdy nie otrzymasz wartości neutralnej.

conversation.summarized

Uruchamia się, gdy ChatLab wygeneruje podsumowanie zakończonej rozmowy.

{
  "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 - wygenerowany tekst podsumowania. Generowanie podsumowania jest asynchroniczne i zależy od konfiguracji podsumowań bota oraz harmonogramu przetwarzania; nie zakładaj stałego czasu dostarczenia.
  • language - wewnętrzny kod ustawień regionalnych bota przekazany do tworzenia podsumowania, na przykład en_US; nie zakładaj, że jest on tożsamy z językiem rozmowy odwiedzającego.

client.summarized

Uruchamia się, gdy ChatLab odświeży profil AI klienta. Profil jest budowany na nowo z poprzedniego profilu oraz podsumowania właśnie zakończonej rozmowy i może zostać wygenerowany po conversation.summarized. Kolejność dostarczania nie jest gwarantowana. Ładunek zawiera adres e-mail, jeśli jest znany, ale ChatLab może przechowywać klientów także bez adresu 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 - identyfikator służący do dopasowania klienta w Twoim CRM-ie. Ma wartość null dla anonimowych odwiedzających, którzy nigdy nie pozostawili adresu, a zdarzenie i tak się dla nich uruchamia - pomiń te powiadomienia, jeśli Twoja integracja opiera się na adresie e-mail.
  • client - rekord kontaktu, który ChatLab posiada dla tej osoby: email, name, phone, countryCode i ipAddress. Każdy klucz jest zawsze obecny; nieznane wartości to null.
  • clientSummary - pełny tekst profilu w formacie zwykłego tekstu, a nie różnica (diff). Zastępuje on jakiekolwiek wcześniejsze podsumowanie, dlatego zapisuj go, nadpisując dane, a nie dopisując do nich.
  • Profil jest przebudowywany wyłącznie dla botów z włączoną pamięcią czatu i tylko dla rozmów, które były nieaktywne wystarczająco długo, aby wygenerować podsumowanie - spodziewaj się tego zdarzenia po kilku minutach od zakończenia rozmowy, a nie natychmiast.

live_chat.requested

Uruchamia się, gdy sztuczna inteligencja przekazuje rozmowę do czatu na żywo (Live Chat), ponieważ odwiedzający poprosił o kontakt z człowiekiem lub bot uznał, że człowiek jest potrzebny.

{
  "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 - obecnie zawsze AI, ponieważ przekazanie jest zawsze wywoływane przez akcję czatu na żywo bota, w tym wtedy, gdy odwiedzający prosi o to własnymi słowami. Traktuj to pole jako otwarty enum: obsługuj nieznane wartości zamiast sztywno zakładać wyłącznie AI.
  • To zdarzenie obejmuje działanie sztucznej inteligencji wnioskującej o przekazanie, a nie każdy sposób otwarcia czatu na żywo przez odwiedzającego. Nie dowodzi ono, że operator faktycznie dołączył do rozmowy.

live_chat.started

Uruchamia się, gdy sesja czatu na żywo zostaje utworzona po przesłaniu formularza przekazania przez odwiedzającego. Operator nie musi jeszcze wtedy być obecny w rozmowie ani na nią odpowiedzieć; nie traktuj tego jako dowodu zaangażowania człowieka.

{
  "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": {}
}
  • Obiekt data jest celowo pusty. Wszystko, czego potrzebujesz, znajduje się w kopercie zdarzenia (envelope): botId identyfikuje chatbota, a conversationId / sessionId wiążą zdarzenie z rozmową. Możesz je otrzymać bez wcześniejszego live_chat.requested, na przykład gdy odwiedzający skorzystał z przycisku czatu na żywo w widgecie.

live_chat.ended

Uruchamia się po zakończeniu sesji czatu na żywo.

{
  "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 - czas trwania sesji czatu na żywo liczony od momentu jej utworzenia, w tym czas oczekiwania na operatora. Pole to jest pomijane w rzadkich przypadkach, gdy sesja kończy się, zanim w ogóle zostanie rozpoczęta.

ai_action.executed

Uruchamia się za każdym razem, gdy bot wykonuje akcję AI - wywołanie zarządzanej integracji lub niestandardowej funkcji API. Jest to zdarzenie o dużym natężeniu: aktywny bot e-commerce może wykonywać setki akcji dziennie, a pojedyncza wypowiedź odwiedzającego może wyzwolić ich kilka. Zasubskrybuj je na dedykowanym punkcie końcowym (endpoint) lub upewnij się, że Twój serwer poradzi sobie z taką liczbą zapytań.

{
  "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 - nazwa wykonanej akcji widziana przez sztuczną inteligencję, na przykład search_products dla zarządzanej integracji lub nazwa nadana niestandardowej akcji API.
  • status - SUCCESS lub ERROR.
  • durationMs - czas trwania akcji w milisekundach. Przydatne do wykrywania wolno działających integracji, zanim odwiedzający zaczną na to narzekać.
  • errorMessage - przyczyna błędu, wypełniana tylko wtedy, gdy status ma wartość ERROR; w przeciwnym razie null.

webhook.test

Wysyłane za pomocą przycisku Send sample event (Wyślij przykładowe zdarzenie) podczas zwykłego sprawdzania łączności. Podpisywane dokładnie tak samo jak prawdziwe zdarzenie.

{
  "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 - stały tekst, zawsze taki sam. Pola koperty zdarzenia zawierają wartości przykładowe, dlatego nigdy nie traktuj danych z webhook.test jako prawdziwych.
  • Jest to jedyny typ zdarzenia, którego nie można zasubskrybować na punkcie końcowym: jest wysyłany na żądanie z panelu administracyjnego i zawsze trafia do punktu końcowego, w który kliknięto, bez względu na to, jakich zdarzeń ten punkt nasłuchuje.

Bezpieczeństwo: weryfikowanie doręczeń

Każde doręczenie zawiera cztery nagłówki:

Nagłówek Wartość
X-ChatLab-Signature sha256=<hex hmac> - podpis HMAC-SHA256 ładunku (payloadu)
X-ChatLab-Timestamp Czas uniksowy w sekundach, kiedy doręczenie zostało podpisane
X-ChatLab-Event Typ zdarzenia, np. lead.created
X-ChatLab-Delivery Unikalny identyfikator doręczenia, zgodny z polem eventId w treści

Podpis jest obliczany jako HMAC-SHA256 z ciągu {timestamp}.{rawBody} przy użyciu Twojego klucza tajnego punktu końcowego (endpoint secret), gdzie {timestamp} to wartość X-ChatLab-Timestamp, a {rawBody} to surowa, nieprzetworzona treść żądania. Zawsze weryfikuj podpis na podstawie surowych bajtów - ponowna serializacja przetworzonego JSON-a zmieni sekwencję bajtów i unieważni podpis.

Aby zabezpieczyć się przed atakami typu replay, odrzucaj doręczenia, których wartość X-ChatLab-Timestamp jest starsza niż 5 minut.

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

Jeśli weryfikacja się nie powiedzie, zwróć kod 401 i odrzuć ładunek. Nigdy nie przetwarzaj niezweryfikowanych doręczeń - każdy, kto pozna Twój adres URL, może przesłać do niego dowolny kod JSON metodą POST.

Działanie doręczania zdarzeń

Przed wdrożeniem webhooków zapoznaj się z poniższymi zasadami doręczania:

  • Odpowiadaj szybko. Twój endpoint musi odpowiedzieć w ciągu 3 sekund - w przeciwnym razie doręczenie uznaje się za nieudane. W tym oknie czasowym zweryfikuj podpis, trwale dodaj zdarzenie do kolejki i potwierdź odbiór kodem 2xx. Wolniejsze zapytania do CRM i operacje biznesowe wykonuj asynchronicznie.
  • Fire-and-forget, at-most-once (co najwyżej raz). Pasujące, włączone endpointy otrzymują co najwyżej jedną próbę doręczenia - nie ma ponownych prób. Wstrzymane endpointy oraz otwarte bezpieczniki (circuit breaker) mogą zablokować nawet tę jedną próbę. Jeśli Twój endpoint nie działa, przekroczy limit czasu lub zwróci status inny niż 2xx, to zdarzenie bezpowrotnie przepada i nie zostanie wysłane ponownie. Webhooki to powiadomienia, a nie zreplikowany magazyn danych: tam, gdzie to możliwe, używaj endpointów konwersacji w Bot Talk API lub eksportu leadów, aby uzgodnić zachowane dane. Management API obejmuje ustawienia bota i statystyki użycia, a nie pełne archiwum zdarzeń. Niektórych zdarzeń nie da się odtworzyć za pomocą tych interfejsów.
  • Bezpiecznik (circuit breaker). Po 5 nieudanych doręczeniach z rzędu dla danej pary endpoint/bot doręczanie dla tej pary zostaje wstrzymane na 5 minut. Zdarzenia występujące w trakcie tej przerwy są odrzucane, a w dzienniku doręczeń pojawiają się wpisy CIRCUIT_OPEN, co pozwala zidentyfikować zablokowane próby. Doręczenia pominięte przez otwarty bezpiecznik nie wliczają się do automatycznego wyłączenia.
  • Automatyczne wyłączanie. Weryfikacja następuje dokładnie w momencie nieudanego doręczenia, nigdy według harmonogramu czasowego. Jeśli doręczenie zakończy się błędem i przez 7 dni nie było żadnego udanego doręczenia - licząc od ostatniego sukcesu lub od daty utworzenia endpointu, jeśli nigdy nie zakończyło się powodzeniem - endpoint zostaje wyłączony, a Ty otrzymujesz powiadomienie e-mail. Pojedynczy kod 2xx w dowolnym momencie resetuje ten licznik. Endpoint, na który nie trafia żaden ruch, nigdy nie zostanie wyłączony, ponieważ nie dochodzi do żadnych błędów. Gdy naprawisz odbiornik, włącz go ponownie w Ustawieniach konta (Account settings); licznik błędów oraz znacznik automatycznego wyłączenia zostaną wyczyszczone w chwili ponownego uruchomienia, a zdarzenia pominięte podczas wyłączenia nie są uzupełniane wstecznie.
  • 410 Gone. Jeśli Twój endpoint odpowie kodem HTTP 410 Gone, ChatLab natychmiast go wyłączy. Użyj tego rozwiązania, aby programowo wycofać endpoint z eksploatacji po stronie odbiorcy.
  • Idempotentność. W normalnych warunkach zduplikowane doręczenia nie powinny występować, ale jeśli Twoje przetwarzanie wymaga ścisłej idempotentności, usuwaj duplikaty po eventId (wartość ta jest również dostępna w nagłówku X-ChatLab-Delivery).

Kolejność doręczania nie jest gwarantowana. Zapisuj identyfikator zdarzenia (event ID) i zadbaj o idempotentność logiki biznesowej. Pamiętaj o własnych zasadach retencji oraz kontroli dostępu do ładunków webhooków, które mogą zawierać dane osobowe i linki do pobierania plików.

Przykłady weryfikacji podpisu wykorzystują bibliotekę Node.js crypto oraz funkcję hash_equals w PHP. Pamiętaj, aby pobrać surową treść żądania (raw body) przed jej przetworzeniem.

Limity

  • Do 10 endpointów webhooków na konto.
  • Czas przechowywania dziennika doręczeń: 14 dni. Starsze wpisy są usuwane automatycznie.

Powiązane artykuły