API reference
Webhooks
View as MarkdownWebhooks push Chat Data events to your own HTTP or HTTPS endpoint (HTTPS recommended) as they happen, so you don't have to poll.
| Event | Fires when |
|---|---|
chat | A chat turn completes. |
live-chat-escalation | A visitor asks for a human agent. |
lead-submission | A visitor submits the lead form. |
user-online | A visitor joins a conversation that is in live chat. |
user-offline | A visitor leaves a conversation that is in live chat. |
Register an endpoint with Add a webhook and remove it with Delete a webhook, or manage webhooks in the chatbot's settings (guide). The response lists each webhook with its secret; keep it on your server.
What a delivery looks like
Each event is a POST to your URL with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | text/plain by default, or application/json when the webhook was added with contentType: application/json (Make and n8n endpoints always get text/plain). The body is JSON either way. |
x-chat-data-topic | The event name, e.g. chat |
x-chat-data-hmac-sha256 | Base64 HMAC-SHA256 of the raw request body, keyed with the webhook's secret |
Verify that a delivery came from Chat Data
Compute the HMAC over the raw body (before JSON parsing) and compare it to the header in constant time.
Node.js
import crypto from 'node:crypto'; // Express: app.post('/chat-data-webhook', express.raw({ type: '*/*' }), handler)function isFromChatData(rawBody, signatureHeader, secret) { const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64'); const a = Buffer.from(expected); const b = Buffer.from(signatureHeader || ''); return a.length === b.length && crypto.timingSafeEqual(a, b);}Python
import base64, hashlib, hmac def is_from_chat_data(raw_body: bytes, signature_header: str, secret: str) -> bool: expected = base64.b64encode(hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()).decode() return hmac.compare_digest(expected, signature_header or "")Respond, retries and disabling
- Reply with any
2xxstatus as soon as you have the event, and do slow work afterwards. - A failed delivery (a network error, a timeout, or a
4xxother than404/410) is retried once, 5 seconds later. 404and5xxresponses are not retried.410 Goneturns the webhook off immediately. After 20 failed deliveries in a row it is also turned off. Once your endpoint is healthy, re-enable the webhook on the chatbot's Settings → Webhooks page, or delete it and add it again (adding the same URL again does not turn it back on).
Event bodies
live-chat-escalation carries chatbot_id, chatbot_name, chatbot_display_name, notification_email, handling_url (where an agent takes over), conversation_id, conversation_start_time, messages, source and lead.
lead-submission carries chatbot_id, chatbot_name, chatbot_display_name and lead (the submitted name, email, phone, source, any custom form fields, and uuid). It has no conversation_id; pass lead.uuid as leadId to Get conversations to find the conversation.
user-online and user-offline carry chatbot_id, chatbot_name, conversation_id and, when known, the visitor's name, email, phone, source and user_uuid.
The chat event body
Sent after each completed chat turn. Only the fields below are stable; new fields may be added.
| Field | Type | Description |
|---|---|---|
| chatbot_idstring | string | ID of the chatbot that produced the response. |
| conversation_idstring | string | UUID of the conversation the message belongs to. |
| messagesarray of object | array of object | Conversation messages preceding the response. |
| messages | string | The role of the message sender (user, assistant, or human for a live chat agent). |
| messages | string | The text content of the message. |
| messages | boolean | Indicates whether the message is a transcript of the realtime voice-to-voice interaction. |
| messages | boolean | Indicates whether the message is upvoted by the user. |
| messages | string | The name of the agent who sent the message, if the role is human. |
| messages | string | The avatar image URL of the agent who sent the message, if the role is human. |
| messages | string | When the message was sent (ISO 8601). |
| messages | string | The ID of the message. |
| messages | string | The model's reasoning for an assistant message, when it was recorded. |
| messages | string | The user's feedback when they downvoted the message. |
| messages | boolean | Indicates whether the message has been read by the user or the agent. |
| messages | array of object | The files attached to the message. |
| messages | string | The name of the file. |
| messages | string | The MIME type of the file. |
| messages | string | The URL of the file in Azure Blob Storage. Use the /get-sas-token endpoint to get a URL with a SAS token for public access. |
| messages | string | The URL of the audio file in Azure Blob Storage, when the message is a transcript of an audio input. Use the /get-sas-token endpoint to get a URL with a SAS token for public access. |
| messages | number | The level of confidence in the generated answer. If no score is present, the answer was not derived from search results. |
| answerobject | object | The assistant message produced by this turn. |
| sourcestring | string | Origin of the request (for example widget, iframe, site or api). |
| cost_creditsnumber | number | Credits charged for this turn. |
| regenerateboolean | boolean | Present and true only when the /chat request used
skipCache: true (a regenerated answer); absent otherwise. To
tell regenerated events apart, check payload.regenerate === true. |
| client_ipstring | string | Caller IP, included only when the chatbot has trackIpAddress enabled. |
| is_live_chatboolean | boolean | Whether the turn was handled by a live human agent. |
| conversation_urlstring | string | URL to view the conversation. Only present when the conversation portal is enabled. |
{ "chatbot_id": "6650f1c2a9e4b83d2c7a1f05", "conversation_id": "3f2b8c1e-9a4d-4e7b-8c2a-5d1e6f7a8b9c", "messages": [ { "role": "user", "content": "What are your opening hours?" } ], "answer": { "role": "assistant", "content": "We are open Monday to Friday, 9am to 6pm." }, "source": "widget", "cost_credits": 1, "client_ip": "203.0.113.42", "is_live_chat": false, "conversation_url": "https://chat.example.com/conversation/QVuByBn5KKcSMnl8s8OZ0Z29X3zvoz-auadhozRjyTIU1pqa5b0EfSncMqr66awipPKWp79_stvOZu_LVd6VQO7vEiWGHRk1Sa_QlDpddCggmKQTVF2oslRss_e2p7S_G0rrKKgMluXg1Ale1SwZjIl5hEzDpRdoALLCnw0G80iqkguX32-C7OuDiXdzQUmnzBegtdnlhTmUhj3m3dmvPy6KabXzYrn4db-42FmntSSK5-9m"}Regenerated answers
A /chat request sent with skipCache: true regenerates the answer, and the chat event it triggers carries regenerate: true. The field is absent (not false) on normal sends, so receivers can dedupe with payload.regenerate === true.