Webhooks overview
Webhooks let ChatLab notify your systems the moment something happens in your chatbots. Instead of polling the Management API or exporting data manually, you register an HTTPS endpoint and ChatLab sends a signed HTTP POST to it in real time - when a visitor leaves a lead, submits a contact form, rates a conversation, requests a human, or when an AI action runs.
Typical uses:
- push new leads straight into your CRM the second they are captured
- notify your team in Slack when a visitor requests live chat
- feed conversation ratings and summaries into your own analytics
- monitor AI action executions and alert on errors
Availability: webhooks are available from the Standard plan up (feature: Webhooks).
Where to configure: in the admin app, open Account settings -> Webhooks (right next to the Management API section). Webhooks are account-level - one endpoint can receive events from all your bots or from a filtered subset.
Setting up an endpoint
- Open Account settings -> Webhooks and click Create endpoint.
- Fill in the endpoint form:
- Name - a label for your own reference, e.g. "CRM sync" or "Slack alerts".
- URL - the HTTPS address ChatLab will POST events to.
- Events - select which event types this endpoint receives (see the catalog below). Select only what you need; high-volume events like
ai_action.executedcan generate a lot of traffic. - Bot filter (optional) - restrict the endpoint to specific bots. Leave empty to receive events from all bots on your account.
- Custom form filter (optional) - routes submissions of one custom form to this endpoint. It narrows the
custom_form.submittedevent only; every other event you subscribe to (leads, contact requests, conversations, live chat, AI actions) is delivered regardless of this setting.
- Submit. The endpoint secret is shown exactly once in the success dialog - copy it now and store it securely. You will need it to verify signatures (see Security below). The plaintext cannot be retrieved later.
Each endpoint also has:
- Enable/disable toggle - pause deliveries without deleting the endpoint. Disabled endpoints silently drop events (they are not queued for later).
- Send sample event - delivers a signed test request to your URL so you can verify your receiver end to end. You can pick the event type and edit the sample values before sending, so your handler sees realistic data. The test arrives as a regular delivery with
eventTypematching your selection (or aswebhook.testfor a plain connectivity check). - Roll secret - generates a new secret and invalidates the old one. Use this if the secret may have leaked. The new secret is again shown only once. Update your receiver before rolling, or deliveries will fail signature verification on your side.
- Delivery log - a per-endpoint list of recent deliveries with timestamp, event type, HTTP status returned by your server, and response time. Failed deliveries and circuit breaker pauses are visible here. The log is retained for 14 days.
Event envelope
Every delivery is an HTTP POST with Content-Type: application/json. The body always has the same envelope; the data object is specific to the event type:
{
"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 per event. Use it for deduplication if your processing must be idempotent.eventType- one of the types documented below; also sent in theX-ChatLab-Eventheader.timestamp- ISO 8601 UTC time the event occurred.botId/botName- the bot the event belongs to.conversationId/sessionId- the conversation context, when applicable.
Event catalog
lead.created
Fires when a visitor submits their contact details - through the lead collection form, the live chat pre-form, or a custom form used for lead collection.
{
"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- how the contact details were captured:LEAD_COLLECTION_FORM(lead collection form),LIVE_CHAT_FORM(live chat pre-form),CONVERSATION(the AI picked the details up during the chat),ADMIN_DATA_UPDATEorUPDATE_CLIENT_CONTEXT(edited on the ChatLab side). Submissions of the human support form never fire this event - they firecontact_form.submittedinstead.email,name,phone- the contact details mapped onto the lead record.- When a custom form is used for lead collection, every field defined on that form is included in
fields, in form order, andformCodeName/formNameidentify the form. With the classic lead form both arenullandfieldsis an empty array. - Each entry in
fieldsis{name, value, type}.nameis the field's technical name, stable across label edits - use it for mapping into your CRM. - For
multichoicefieldsvalueis an array of the selected options. Checkbox fields are individual entries with"true"/"false"values. - For
filefieldsvalueis a download link to the uploaded file; the webhook never carries file contents. pageUrl- the page the visitor was on when they submitted.
contact_form.submitted
Fires when a visitor submits the human support contact form or a custom form used for human 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- the address the visitor left, and the one your support team should reply to.source-CUSTOM_FORMwhen a custom form backs the contact form,CONTACT_FORMfor the built-in one.formCodeName/formName- identify the custom form behind the request; both arenullfor the built-in form.- When a custom form is used, every field defined on that form is included in
fields(same{name, value, type}format aslead.created). With the built-in contact form onlyemailandmessageare populated,fieldsis an empty array and the form identifiers arenull. message- the mapped message field, or all filled values joined together when the form defines no message field.
custom_form.submitted
Fires for every custom form submission, regardless of the form's purpose. Note that forms whose purpose is lead collection or human contact also fire their dedicated lead.created / contact_form.submitted event - subscribe to one or the other depending on whether you want the generic or the specialized view, and deduplicate by conversationId + timestamp if you subscribe to both.
{
"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- the stable machine name of the form, unchanged when you rename the form; use it to route submissions in your own system.formNameis the display label shown to visitors.fieldsuses the same{name, value, type}entries aslead.created: multichoice values are arrays, file values are download links.purpose-STANDALONE,LEAD_COLLECTIONorHUMAN_CONTACT, depending on how the form is wired to the chatbot.
conversation.started
Fires when a visitor sends the first message of a new 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- the exact text of the visitor's opening message.nullif the conversation was opened without message content.chatSource- the channel the conversation came in on:WIDGET,WHATSAPP,MESSENGER,VOICE,VOICE_PHONE,API,BOOKING,AIRBNBorIDOBOOKING.byAdmin-truewhen the conversation comes from the chatbot preview inside the ChatLab admin panel rather than from a real visitor. Use it to keep your own test chats out of your CRM.countryCode- ISO country code resolved from the visitor's IP address,nullwhen it could not be determined.ipAddress- the visitor's IP address as seen by ChatLab,nullwhen unavailable. Treat it as personal data under GDPR and store it only if you have a lawful basis.
conversation.rated
Fires when a visitor rates a bot reply with thumbs up or down (see Conversation rating).
{
"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-POSITIVEorNEGATIVE. Clearing a rating does not fire the event, so you never receive a neutral value.
conversation.summarized
Fires when ChatLab generates a summary of a finished conversation.
{
"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"
}
}
summary- the generated summary text. Summaries are produced a few minutes after the conversation goes idle, so this event arrives later than the rest of the conversation events.language- ISO code of the language the summary was written in, following the conversation language.
client.summarized
Fires when ChatLab refreshes the AI profile of a client. The profile is rebuilt from the previous profile plus the summary of the conversation that just ended, so this event follows conversation.summarized for the same conversation. Clients are identified by e-mail, which is why the address is repeated at the top level of data.
{
"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- the identifier to match the client against your own CRM. It isnullfor anonymous visitors who never left an address, and the event still fires for them - skip those deliveries if your integration is keyed on e-mail.client- the contact record ChatLab holds for this person:email,name,phone,countryCodeandipAddress. Every key is always present; unknown values arenull.clientSummary- the full profile text as plain text, not a diff. It replaces whatever the previous summary was, so store it as an overwrite rather than appending.- The profile is rebuilt only for bots with chat memory enabled, and only for conversations that were idle long enough to be summarized - expect this event minutes after the conversation ends, not immediately.
live_chat.requested
Fires when the AI hands the conversation over to live chat, either because the visitor asked for a human or because the bot decided a human was needed.
{
"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- currently alwaysAI, because the handoff is always raised by the bot's live chat action, including when the visitor asks for it in plain words. Treat it as an open enum: handle unknown values instead of asserting onAI.- The event says a handover was requested, not that an operator picked it up. Wait for
live_chat.startedfor that.
live_chat.started
Fires when an operator joins and the live chat session actually begins.
{
"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": {}
}
datais intentionally empty. Everything you need is in the envelope:botIdidentifies the chatbot andconversationId/sessionIdtie the event to the conversation you already receivedlive_chat.requestedfor.
live_chat.ended
Fires when the live chat session ends.
{
"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- how long the operator was in the conversation, counted from the moment the session started. The field is omitted in the rare case where a session ends without ever having been started.
ai_action.executed
Fires every time the bot executes an AI action - a managed integration call or a custom API function. This is a high-volume event: an active e-commerce bot can execute hundreds of actions per day, and a single visitor turn can trigger several. Subscribe to it on a dedicated endpoint, or be sure your receiver can absorb the 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- the name of the executed action as the AI sees it, for examplesearch_productsfor a managed integration or the name you gave a custom API action.status-SUCCESSorERROR.durationMs- how long the action took, in milliseconds. Useful for spotting a slow integration before visitors complain about it.errorMessage- the failure reason, populated only whenstatusisERROR;nullotherwise.
webhook.test
Sent by the Send sample event button when you run a plain connectivity check. Signed exactly like a real event.
{
"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- fixed text, always the same. The envelope fields carry sample values, so never treat awebhook.testdelivery as real data.- This is the only event type you cannot subscribe to on an endpoint: it is sent on demand from the admin panel and always reaches the endpoint you clicked, no matter which events it listens to.
Security: verifying deliveries
Every delivery carries four headers:
| Header | Value |
|---|---|
X-ChatLab-Signature |
sha256=<hex hmac> - HMAC-SHA256 signature of the payload |
X-ChatLab-Timestamp |
Unix time in seconds when the delivery was signed |
X-ChatLab-Event |
The event type, e.g. lead.created |
X-ChatLab-Delivery |
Unique delivery id, equal to the body's eventId |
The signature is computed as HMAC-SHA256 over the string {timestamp}.{rawBody} using your endpoint secret, where {timestamp} is the value of X-ChatLab-Timestamp and {rawBody} is the raw, unparsed request body. Always verify against the raw bytes - re-serializing parsed JSON will change the byte sequence and break the signature.
To protect against replay attacks, reject deliveries whose X-ChatLab-Timestamp is older than 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 (!signature || !timestamp) 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 + '.' + 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 === '') {
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);
}
If verification fails, respond with 401 and discard the payload. Never process unverified deliveries - anyone who discovers your URL can POST arbitrary JSON to it.
Delivery behavior
Understand these guarantees before building on webhooks:
- Respond fast. Your endpoint must respond within 3 seconds or the delivery counts as failed. Respond
2xximmediately and process the payload asynchronously (queue it, then acknowledge) - do not do CRM calls or database writes before answering. - Fire-and-forget, at-most-once. Each event gets exactly one delivery attempt - there are no retries. If your endpoint is down, times out, or returns a non-2xx status, that event is gone and will not be redelivered. Webhooks are notifications, not a replicated data store: when you need guaranteed completeness, reconcile against the Management API or your lead exports.
- Circuit breaker. After 5 consecutive failed deliveries for a bot, deliveries for that bot pause for 5 minutes. Events occurring during the pause are dropped, and the delivery log shows
CIRCUIT_OPENentries so you can see exactly when and why traffic was suppressed. Deliveries skipped by an open circuit do not count towards auto-disable. - Auto-disable. The check runs at the moment a delivery fails, never on a timer. If a delivery fails and there has been no successful delivery for 7 days - counted from the last success, or from the endpoint's creation date if it has never succeeded - the endpoint is switched off and you receive an email notification. A single
2xxat any point resets that clock. An endpoint that receives no traffic is never disabled, because nothing fails. Re-enable it from Account settings once your receiver is fixed; the failure counter and the auto-disable stamp are cleared when you switch it back on, and events missed while it was off are not backfilled. - 410 Gone. If your endpoint responds with HTTP
410 Gone, ChatLab disables it immediately. Use this to programmatically decommission an endpoint from the receiving side. - Idempotency. Duplicate deliveries are not expected in normal operation, but if your processing must be strictly idempotent, deduplicate by
eventId(also available in theX-ChatLab-Deliveryheader).
Limits
- Up to 10 webhook endpoints per account.
- Delivery log retention: 14 days. Older entries are removed automatically.
Related articles
- Lead collection - the form behind
lead.created - Human Support Contact form - the form behind
contact_form.submitted - Live Chat - the flow behind the
live_chat.*events - Conversation rating - the thumbs up/down behind
conversation.rated - AI Actions - the integrations behind
ai_action.executed - Chat API - in-browser widget callbacks (client-side counterpart to webhooks)
- Management API - REST API for bot management and usage data