Webhook
Когда пользователь создаёт обращение в чате, Kavchat может сразу отправить его в вашу систему: службу поддержки, CRM или боту в Slack. Каждая отправка подписывается вашим секретным ключом.
Webhook входит в тариф Премиум.
Настройка
- Создайте на сервере HTTPS-адрес, который принимает
POSTс телом в формате JSON. - В панели управления откройте Настройки и укажите адрес в разделе Webhook.
- Проверяйте подпись каждого запроса, как описано ниже, и отвечайте любым кодом статуса
2xx.
Адрес должен начинаться с https:// и быть общедоступным. Адреса в частных сетях блокируются.
Содержимое сообщения
Kavchat отправляет два типа событий: ticket.created для обращения, отправленного через форму обращения, и lead.created для данных, которые посетитель оставил на шаге сбора лидов в сценарии. Поле data содержит обращение или лид в том же формате, который возвращает серверный API, включая историю переписки.
POST /kavchat/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Kavchat-Webhook/1
Kavchat-Signature: t=1792915200,v1=5f2b7c...e91a{
"type": "ticket.created",
"data": {
"id": "6718f0c2a4b5c6d7e8f90123",
"number": 128,
"status": "open",
"name": "Dana",
"email": "dana@example.com",
"phone": "+972501234567",
"userId": "user_123",
"topicId": "billing",
"topicName": "Billing",
"scriptId": "returns",
"sessionId": "k3J9xQ2mP7aB",
"message": "My refund has not arrived yet.",
"transcript": [
{ "from": "bot", "text": "Hi! How can I help?", "stepId": "start", "at": "2026-10-25T09:14:02.000Z" },
{ "from": "user", "text": "Returns", "at": "2026-10-25T09:14:05.000Z" }
],
"createdAt": "2026-10-25T09:15:40.000Z",
"updatedAt": "2026-10-25T09:15:40.000Z"
}
}Если пользователь идентифицирован, userId содержит значение поля sub из токена идентификации, иначе null. Поля name, email, phone и поля темы также могут иметь значение null.
Лиды и персональные данные
Сценарий может запрашивать у посетителя имя, телефон, электронную почту, число или произвольный текст, проверять каждый ответ и сохранять его в переменную. Шаг сбора лидов отправляет эти данные в раздел лидов в панели управления. В событии lead.created поле data.variables содержит все собранные значения с именами переменных в качестве ключей. Основные значения также доступны в полях data.name, data.email и data.phone. Номера телефонов передаются в международном формате (E.164).
Эти сведения относятся к персональным данным. Kavchat хранит их только в том лиде, обращении или чате с оператором, к которому они относятся, и никогда не сохраняет в отчётах или журналах. Лиды удаляются по истечении выбранного вами срока хранения (30 дней на бесплатном тарифе; на тарифе Премиум можно также хранить их бессрочно). Отдельный лид можно удалить в любой момент. Если вы собираете персональные данные, укажите это в своей политике конфиденциальности. В тексте согласия на шаге сбора лидов объясните посетителям, как вы будете использовать их данные.
Заголовок Kavchat-Signature
Заголовок имеет вид t=<unix seconds>,v1=<hex>, где:
t— время отправки в секундах Unix.v1—HMAC-SHA256(secretKey, "<t>.<raw body>")в шестнадцатеричном представлении. В качестве ключа используется ваш полный секретный ключ.
Для проверки вычислите тот же HMAC по исходному телу запроса, в точности в том виде, в котором оно пришло, выполните сравнение за постоянное время и отклоняйте запросы старше 5 минут. Если тело запроса было разобрано и заново преобразовано в JSON, подпись не совпадёт.
Проверка с помощью SDK
Функция verifyWebhook выполняет все эти действия и возвращает разобранное событие. При сбое она выбрасывает исключение с кодом malformed_header, timestamp_out_of_range, invalid_signature или invalid_payload. В этих случаях отвечайте кодом HTTP 400. Установка SDK описана в разделе Идентификация пользователей.
import express from 'express';
import { Kavchat, WebhookVerificationError } from '@kavchat/node';
const kavchat = new Kavchat(process.env.KAVCHAT_SECRET_KEY!);
// Use the raw body. A body that was parsed and serialized again will not match the signature.
app.post('/kavchat/webhook', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = kavchat.verifyWebhook(req.body, req.get('Kavchat-Signature'));
if (event.type === 'ticket.created') console.log('New ticket', event.data.number);
res.sendStatus(200);
} catch (err) {
if (err instanceof WebhookVerificationError) return res.status(400).send(err.code);
throw err;
}
});from flask import Flask, request
from kavchat import WebhookVerificationError
app = Flask(__name__)
@app.post("/kavchat/webhook")
def webhook():
try:
# raw bytes: request.get_data() in Flask, request.body in Django
event = kavchat.verify_webhook(request.get_data(), request.headers.get("Kavchat-Signature"))
except WebhookVerificationError as err:
return err.code, 400
if event["type"] == "ticket.created":
print("New ticket", event["data"]["number"])
return "", 200use Kavchat\WebhookVerificationException;
try {
$event = $kavchat->verifyWebhook(
file_get_contents('php://input'),
$_SERVER['HTTP_KAVCHAT_SIGNATURE'] ?? null,
);
} catch (WebhookVerificationException $e) {
http_response_code(400);
exit($e->errorCode);
}
if ($event->type === 'ticket.created') {
error_log('New ticket ' . $event->data->number);
}@PostMapping("/kavchat/webhook")
public ResponseEntity<String> webhook(@RequestBody byte[] body,
@RequestHeader(value = "Kavchat-Signature", required = false) String signature) {
try {
WebhookEvent event = kavchat.verifyWebhook(body, signature);
if ("ticket.created".equals(event.getType())) log.info("New ticket {}", event.getData().getNumber());
return ResponseEntity.ok("");
} catch (WebhookVerificationException e) {
return ResponseEntity.badRequest().body(e.getCode());
}
}app.MapPost("/kavchat/webhook", async (HttpRequest request, KavchatClient kavchat) =>
{
using var ms = new MemoryStream();
await request.Body.CopyToAsync(ms);
try
{
var evt = kavchat.VerifyWebhook(ms.ToArray(), request.Headers[KavchatWebhook.SignatureHeader]);
if (evt.Type == "ticket.created") Console.WriteLine($"New ticket {evt.Data.Number}");
return Results.Ok();
}
catch (WebhookVerificationException e)
{
return Results.BadRequest(e.Code);
}
});http.HandleFunc("/kavchat/webhook", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
http.Error(w, "read error", http.StatusBadRequest)
return
}
event, err := client.VerifyWebhook(body, r.Header.Get(kavchat.SignatureHeader))
var werr *kavchat.WebhookError
if errors.As(err, &werr) {
http.Error(w, werr.Code, http.StatusBadRequest)
return
}
if event.Type == "ticket.created" {
log.Printf("new ticket %d", event.Data.Number)
}
})class KavchatWebhooksController < ActionController::API
def create
event = KAVCHAT.verify_webhook(request.raw_post, request.headers["Kavchat-Signature"])
Rails.logger.info("New ticket #{event.data.number}") if event.type == "ticket.created"
head :ok
rescue Kavchat::WebhookVerificationError => e
render plain: e.code, status: :bad_request
end
endПо умолчанию допустимое отклонение времени составляет 5 минут (300 секунд). В каждом SDK его можно изменить, например kavchat.verifyWebhook(body, header, { toleranceSeconds: 300 }) в Node.js.
Без SDK
import crypto from 'node:crypto';
// header: "t=<unix seconds>,v1=<hex>", rawBody: the request body exactly as received
function verifyKavchatSignature(rawBody, header, secretKey) {
const parts = Object.fromEntries(String(header).split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes
const expected = crypto.createHmac('sha256', secretKey).update(`${t}.${rawBody}`).digest('hex');
const given = Buffer.from(parts.v1 || '', 'hex');
return given.length === 32 && crypto.timingSafeEqual(given, Buffer.from(expected, 'hex'));
}Отправка и повторные попытки
- Отправка считается успешной, если ваш сервер отвечает кодом статуса
2xxв течение 8 секунд. - При неудачной отправке повторные попытки выполняются через 5 секунд, 30 секунд и 2 минуты: всего четыре попытки.
- На пользователя это не влияет: обращение в любом случае сохраняется в разделе обращений в панели управления, и его всегда можно получить через серверный API.
- Если ваш сервер получил запрос, но ответил позже чем через 8 секунд, запрос будет отправлен повторно. Используйте
idобращения, чтобы отфильтровывать дубликаты.