Webhooks

Events on your address: the list, the headers, the HMAC signature check, retries and loop protection.

Events #

EventWhenIn data
conversation.startedthe visitor started a conversationconversation
message.createda new message in either directionconversation, message
conversation.closedthe conversation was closedconversation, reason
visitor.identifiedthe site passed visitor detailsconversation
webhook.testa test from the dashboard, cannot be subscribed to

The names are final. A new event is added next to the old ones, and an old one is never renamed: your handlers are written against these names.

What the request looks like #

The conversation and message objects are the same ones the API serves: one parser works for both.

Request
POST /your-handler HTTP/1.1
Content-Type: application/json
X-WidgetChat-Event: message.created
X-WidgetChat-Delivery: message.created:8f1c…
X-WidgetChat-Attempt: 1
X-WidgetChat-Timestamp: 1755878400
X-WidgetChat-Signature: sha256=6b2f…

{"id": "message.created:8f1c…",
 "event": "message.created",
 "createdAt": "2026-08-22T12:00:00Z",
 "workspaceKey": "example-ru",
 "data": {"conversation": { … }, "message": { … }}}

Checking the signature #

What is signed is the string timestamp + "." + body, not the body alone: a signature over a bare body could be replayed a day later and the handler would not tell the replay from a fresh event.

Check three things: the signature matches in a constant-time comparison; the timestamp is no more than five minutes away from your clock; the id from X-WidgetChat-Delivery has not been seen before.

Example call
// Go
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(r.Header.Get("X-WidgetChat-Timestamp") + "." + string(body)))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
ok := hmac.Equal([]byte(expected), []byte(r.Header.Get("X-WidgetChat-Signature")))
Request
# PHP
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
$ok = hash_equals($expected, $signature);

Answers and retries #

We wait 20 seconds for the answer. Answer right away and do the work afterwards — a slow handler counts as a failure. Every attempt is visible in the delivery log in the dashboard: the event, the attempt number, the response code, the error text.

Your answerWhat we do
2xxcounted as accepted, we send no more
4xxno retry: the recipient will not change its mind
5xx, timeout, dropped connection3 attempts, a 5 second pause doubling after that

Loop protection #

A reply sent by your own program also produces message.created. Look at the origin field inside message: api is you, telegram is an operator from the group, bitrix24, amocrm and retailcrm are a manager from a CRM, widget is the visitor.

Requirements for the address #

HTTPS only and an external address only: the event carries a customer's conversation. An address inside a private network is not accepted.