Skip to content

API reference

Webhooks

View as Markdown

Webhooks push Chat Data events to your own HTTP or HTTPS endpoint (HTTPS recommended) as they happen, so you don't have to poll.

EventFires when
chatA chat turn completes.
live-chat-escalationA visitor asks for a human agent.
lead-submissionA visitor submits the lead form.
user-onlineA visitor joins a conversation that is in live chat.
user-offlineA 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:

HeaderValue
Content-Typetext/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-topicThe event name, e.g. chat
x-chat-data-hmac-sha256Base64 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 2xx status as soon as you have the event, and do slow work afterwards.
  • A failed delivery (a network error, a timeout, or a 4xx other than 404/410) is retried once, 5 seconds later.
  • 404 and 5xx responses are not retried.
  • 410 Gone turns 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.

FieldDescription
chatbot_idstringID of the chatbot that produced the response.
conversation_idstringUUID of the conversation the message belongs to.
messagesarray of objectConversation messages preceding the response.
messages[].rolestringThe role of the message sender (user, assistant, or human for a live chat agent).
messages[].contentstringThe text content of the message.
messages[].isTranscriptbooleanIndicates whether the message is a transcript of the realtime voice-to-voice interaction.
messages[].upvotebooleanIndicates whether the message is upvoted by the user.
messages[].namestringThe name of the agent who sent the message, if the role is human.
messages[].avatarstringThe avatar image URL of the agent who sent the message, if the role is human.
messages[].timestampstringWhen the message was sent (ISO 8601).
messages[].midstringThe ID of the message.
messages[].reasoningstringThe model's reasoning for an assistant message, when it was recorded.
messages[].downvoteFeedbackstringThe user's feedback when they downvoted the message.
messages[].readbooleanIndicates whether the message has been read by the user or the agent.
messages[].filesarray of objectThe files attached to the message.
messages[].files[].namestringThe name of the file.
messages[].files[].typestringThe MIME type of the file.
messages[].files[].urlstringThe 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[].audioUrlstringThe 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[].scorenumberThe level of confidence in the generated answer. If no score is present, the answer was not derived from search results.
answerobjectThe assistant message produced by this turn.
sourcestringOrigin of the request (for example widget, iframe, site or api).
cost_creditsnumberCredits charged for this turn.
regeneratebooleanPresent 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_ipstringCaller IP, included only when the chatbot has trackIpAddress enabled.
is_live_chatbooleanWhether the turn was handled by a live human agent.
conversation_urlstringURL 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.

Last updated October 14, 2026