Вебхук событий: обращения, заявки и звонки AI-агента в вашу аналитику
Три события, по одному на бизнес-факт: клиент обратился впервые (написал в мессенджер или позвонил), заявка передана менеджеру, звонок завершён. На каждое платформа отправляет HTTP POST с JSON на ваш эндпоинт. В каждом событии есть channel (в какой канал написали или позвонили: тип, id и название бота или транка), agent (какой AI-агент обработал), contact и lead, а у обращения из рекламы — lead.click_id. Это отдельная интеграция от webhook-назначения лидов; их можно включить вместе, приёмник у обеих один и тот же по заголовкам и стилю payload.
Подключение
- Личный кабинет → Настройки → Интеграции → «Вебхук событий».
- Укажите
https://-URL, скопируйте сгенерированный секрет, отметьте нужные события. - Нажмите «Проверить подключение» — придёт событие
webhook.test, ответьте2xx. - Сохраните. Дальше события идут автоматически; последние доставки и ошибки видны там же.
Требования к эндпоинту
- Только
https://. - Отвечать
2xxв пределах ~10 секунд. Обрабатывайте асинхронно — сначала ответьте, потом считайте. - Идемпотентность по
event_id— повтор доставки несёт тот же идентификатор. - Ветвиться по
eventи отвечать2xxна неизвестные типы (включаяwebhook.test).
Каталог событий
Подписка точная: приходят только отмеченные типы; новые типы, которые появятся в платформе позже, не включаются автоматически. Раскройте событие — внутри ровно тот JSON, который прилетит на ваш URL.
▸lead.createdНовое обращениеЛиды
Клиент впервые написал в мессенджер (Telegram, WhatsApp, MAX) или позвонил — создан лид. Одно событие на обращение: то, что нужно для конверсии «написал в мессенджер» в Яндекс Директе (lead.click_id из ссылки на бота). data.type: new, re_engaged, upsell.
{
"event": "lead.created",
"event_id": "0b7e6c4a-2f1d-4e8b-9a3c-5d6e7f8a9b0c",
"api_version": "1",
"occurred_at": "2026-09-14T09:40:00Z",
"sent_at": "2026-09-14T10:15:04.001Z",
"organization_id": 2,
"demo_mode": null,
"channel": {
"type": "telegram",
"id": 12,
"name": "Бот продаж"
},
"agent": {
"id": 21,
"name": "Фитнес — чат"
},
"contact": {
"id": 7,
"first_name": "Иван",
"last_name": "Петров",
"phone": null,
"callback_phone": "+79995554433",
"email": null,
"telegram": "@ivan_petrov",
"whatsapp": null,
"channel_user_id": null,
"channel_handle_label": null,
"company": null,
"role": "unknown",
"channel": "telegram"
},
"lead": {
"id": 461,
"url": "https://salesworkshop.ai/leads/461",
"type": "new",
"status": "new",
"mode": "ai",
"is_hot": false,
"goal_reached": false,
"summary": null,
"channel": "telegram",
"call_resolution": null,
"direction": "inbound",
"click_id": "16234567890123456789",
"click_source": "yandex",
"start_param": "yclid_16234567890123456789",
"created_at": "2026-09-14T09:40:00Z",
"qualification": {
"current_situation": null,
"volume": null,
"goals": null,
"timeline": null,
"budget_level": null
},
"custom_attributes": {}
},
"chat": {
"id": 55
},
"actor": null,
"data": {
"message_id": 899,
"content": "Новый лид создан",
"lead_id": 461,
"type": "new"
}
}▸lead.handoffКвалифицирован и передан менеджеруЛиды
AI квалифицировал клиента и передал заявку менеджеру (data.reason: ready_to_buy, human_request, …); в блоках contact и lead — собранные данные и квалификация.
{
"event": "lead.handoff",
"event_id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"api_version": "1",
"occurred_at": "2026-09-14T10:21:45Z",
"sent_at": "2026-09-14T10:15:04.001Z",
"organization_id": 2,
"demo_mode": null,
"channel": {
"type": "telegram",
"id": 12,
"name": "Бот продаж"
},
"agent": {
"id": 21,
"name": "Фитнес — чат"
},
"contact": {
"id": 7,
"first_name": "Иван",
"last_name": "Петров",
"phone": null,
"callback_phone": "+79995554433",
"email": null,
"telegram": "@ivan_petrov",
"whatsapp": null,
"channel_user_id": null,
"channel_handle_label": null,
"company": null,
"role": "unknown",
"channel": "telegram"
},
"lead": {
"id": 461,
"url": "https://salesworkshop.ai/leads/461",
"type": "new",
"status": "active",
"mode": "manual",
"is_hot": true,
"goal_reached": false,
"summary": "Интересует годовой абонемент, спрашивал про бассейн и рассрочку.",
"channel": "telegram",
"call_resolution": null,
"direction": "inbound",
"click_id": "16234567890123456789",
"click_source": "yandex",
"start_param": "yclid_16234567890123456789",
"created_at": "2026-09-14T09:40:00Z",
"qualification": {
"current_situation": "Ходит в зал у дома, не устраивает оборудование",
"volume": "Годовой абонемент, 1 человек",
"goals": "Похудеть к лету, нужен бассейн",
"timeline": "Готов купить на этой неделе",
"budget_level": "до 40 000 ₽"
},
"custom_attributes": {
"club": "Ленина"
}
},
"chat": {
"id": 55
},
"actor": {
"type": "agent",
"id": "21",
"name": "Фитнес — чат"
},
"data": {
"message_id": 910,
"content": "AI запросил передачу менеджеру: ready_to_buy",
"reason": "ready_to_buy",
"from_mode": "ai",
"to_mode": "manual"
}
}▸call.endedЗвонок завершёнЗвонки
Результат звонка: длительность, исход, причина завершения, перевод на менеджера (lead.call_resolution), резюме.
{
"event": "call.ended",
"event_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f7a",
"api_version": "1",
"occurred_at": "2026-09-14T11:04:51Z",
"sent_at": "2026-09-14T10:15:04.001Z",
"organization_id": 2,
"demo_mode": null,
"channel": {
"type": "voice",
"id": 3,
"name": "Sipuni — офис"
},
"agent": {
"id": 32,
"name": "Фитнес — звонки"
},
"contact": {
"id": 7,
"first_name": "Иван",
"last_name": "Петров",
"phone": "+79991234567",
"callback_phone": null,
"email": null,
"telegram": null,
"whatsapp": null,
"channel_user_id": null,
"channel_handle_label": null,
"company": null,
"role": "unknown",
"channel": "voice"
},
"lead": {
"id": 462,
"url": "https://salesworkshop.ai/leads/462",
"type": "new",
"status": "active",
"mode": "ai",
"is_hot": true,
"goal_reached": false,
"summary": "Позвонил узнать про абонемент, договорились о пробной тренировке в субботу.",
"channel": "phone",
"call_resolution": null,
"direction": "inbound",
"click_id": null,
"click_source": null,
"start_param": null,
"created_at": "2026-09-14T09:40:00Z",
"qualification": {
"current_situation": "Ходит в зал у дома, не устраивает оборудование",
"volume": "Годовой абонемент, 1 человек",
"goals": "Похудеть к лету, нужен бассейн",
"timeline": "Пробная тренировка в субботу",
"budget_level": "до 40 000 ₽"
},
"custom_attributes": {
"club": "Ленина"
}
},
"chat": {
"id": 56
},
"actor": {
"type": "system",
"id": null,
"name": "Голосовой вызов"
},
"data": {
"message_id": 1201,
"content": "Голосовой вызов: Заинтересован (2:41)",
"voice_call_id": "3f2a9c1e-7b4d-4e6a-9c8b-1d2e3f4a5b6c",
"duration_seconds": 161,
"outcome": "interested",
"outcome_label": "Заинтересован",
"transcript_count": 18,
"direction": "inbound",
"termination_reason": "USER_HANGUP",
"termination_detail": null
}
}Ответы агента, отдельные сообщения и реплики звонка, смены статусов и служебные события ленты не публикуются — каталог расширяется по запросу. Ссылка на запись звонка наружу не передаётся.
Рекламные конверсии: «написал в мессенджер» с идентификатором клика
Событие lead.created приходит один раз на обращение — в момент первого сообщения клиента в мессенджере (или первого звонка), до ответа агента. Это и есть конверсия «написал в мессенджер»: не нужно дедуплицировать сообщения одного диалога. Чтобы связать её с кликом по объявлению, донесите идентификатор клика до бота.
- Ведите рекламу на бота ссылкой с идентификатором клика в параметре
start:https://t.me/<бот>?start=yclid_<yclid>(в MAX —https://max.ru/<бот>?start=yclid_<yclid>) для Яндекс Директа,gclid_…для Google Ads,fbclid_…для Meta,ttclid_…для TikTok,rb_clickid_…для VK. Рекламные сети подставляют клик только в адрес вашего сайта, поэтому обычно делают редирект: объявление ведёт на страницу сайта, она читает идентификатор из адреса и перенаправляет на ссылку бота с ним (в параметре допустимы только буквы, цифры,_и-, до 64 символов). - В Telegram первое сообщение клиента приходит как
/start yclid_…, в MAX параметр приходит при нажатии «Начать»; платформа сохраняет его на лиде:lead.click_id— сам идентификатор,lead.click_source— сеть по префиксу (yandex,google,meta,tiktok,vk),lead.start_param— сырое значение. Без префикса платформа не гадает: голое число, промокод или метка кампании сохраняются только вstart_param, аclick_idостаётсяnull. Первый клик выигрывает: повторные/startтого же лида ничего не меняют. В MAX параметр приходит только при первом запуске бота — клиент, который уже писал боту раньше, придёт по рекламной ссылке безclick_id. - Подпишитесь на
lead.created, отфильтруйтеchannel.type(telegram, whatsapp_baileys, max — илиvoice, если первым был звонок) и загрузите конверсию в рекламный кабинет поlead.click_idиoccurred_at. Без идентификатора клика остаётся сопоставление поcontact.phone/contact.telegramчерез загрузку CRM-конверсий.
Как понять исход звонка
Типичная задача партнёра: сам звонок и запись он получает от своей телефонии, а от платформы нужен результат — перевёл ли агент на менеджера, договорился ли, квалифицировал ли сам. Подпишитесь на lead.created, call.ended и lead.handoff и читайте:
| Что нужно узнать | Где это в событиях |
|---|---|
| Клиент обратился впервые (написал или позвонил), в какой канал, какой агент | lead.created: channel.type, channel.name, agent.name, contact, lead.type, lead.click_id |
| Перевёл на менеджера | call.ended: data.termination_reason = TRANSFERRED; lead.call_resolution = transferred |
| Клиент просил перезвонить | call.ended: data.outcome = callback_requested; lead.call_resolution = callback_requested; contact.callback_phone |
| Квалифицировал / оставил заявку | lead.handoff: data.reason (ready_to_buy, human_request, …); блоки contact и lead с квалификацией |
| Не договорился | call.ended: data.outcome ∈ not_interested | wrong_number | hung_up | no_answer и нет lead.handoff по этому lead.id |
| Длительность, кто положил трубку | call.ended: data.duration_seconds, data.termination_reason (USER_HANGUP, AGENT_HANGUP, …) |
| Итог словами | lead.summary в call.ended и lead.handoff |
Сопоставление с записями вашей телефонии — по contact.phone (номер, с которого позвонили / который набрали) и времени: occurred_at у lead.created для первого звонка, occurred_at и data.duration_seconds у call.ended; data.voice_call_id — внутренний идентификатор платформы, не SIP Call-ID. Все события одного лида несут один lead.id; lead.handoff приходит через несколько секунд после call.ended.
Аутентификация
Каждый запрос несёт заголовок Authorization: Bearer <секрет>. Секрет (UUID) генерируется в личном кабинете при подключении. Сравнивайте его со своим константным по времени сравнением и отклоняйте запрос при несовпадении.
PHP
$expected = getenv('SWS_WEBHOOK_SECRET');
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = preg_replace('/^Bearer\s+/i', '', $header);
if (!hash_equals($expected, $token)) {
http_response_code(401);
exit;
}Node.js
const crypto = require("crypto");
function verify(req) {
const expected = process.env.SWS_WEBHOOK_SECRET;
const token = (req.headers.authorization || "").replace(/^Bearer\s+/i, "");
const a = Buffer.from(token);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Заголовки
| Заголовок | Значение |
|---|---|
| Content-Type | application/json |
| Authorization | Bearer <секрет> — всегда |
| X-SWS-Event | тип события, напр. lead.created |
| X-SWS-Event-Id | UUID события — одинаков на всех повторах (ключ дедупликации) |
| X-SWS-Delivery | номер попытки: 1 для первой, растёт на повторах |
Тело запроса
Один JSON-объект: общий конверт + блоки channel, agent, contact, lead, chat, actor и data с полями конкретного события (примеры — в каталоге выше). Скалярные поля всегда присутствуют, null — если значение неизвестно. Блоки contact / lead / channel / agent отражают состояние на момент доставки (обычно секунды после события); сами переходы — в data.
| Поле | Тип | Описание |
|---|---|---|
| event | string | Тип события из каталога (плюс webhook.test при проверке подключения). |
| event_id | string (UUID) | Идентификатор события. Стабилен на всех повторах доставки — ключ дедупликации. |
| api_version | string | Версия контракта. Сейчас "1". |
| occurred_at | string (ISO-8601, UTC) | Когда событие произошло. |
| sent_at | string (ISO-8601, UTC) | Когда отправлена эта попытка доставки. |
| organization_id | number | ID организации в платформе. |
| demo_mode | null | "live" | null для реального лида; "live" — тест агента из вкладки «Превью» (dry-тесты не отправляются). У live-превью contact.first_name с префиксом «[Demo] ». |
| channel | object | Канал события: { type, id, name }. type — telegram, whatsapp_baileys, max, preview, voice; id и name — конкретный бот или транк из настроек каналов. |
| agent | object | null | AI-агент, который обработал событие: { id, name }. Для нового обращения — агент, назначенный на этот канал. |
| contact | object | null | id, first_name, last_name, phone, callback_phone, email, telegram, whatsapp, channel_user_id, channel_handle_label, company, role, channel. Синтетические id превью никогда не приходят как телефон. |
| lead | object | null | id, url, type, status, mode, is_hot, goal_reached, summary, channel, direction (inbound | outbound | null), call_resolution (transferred | callback_requested | null), click_id, click_source, start_param, created_at, qualification {5 полей BANT}, custom_attributes {key: value}. |
| lead.click_id | string | null | Идентификатор клика по объявлению из ссылки на бота (см. раздел про рекламные конверсии): значение после префикса yclid_, gclid_, fbclid_, ttclid_, rb_clickid_ или msclkid_. null, если префикса нет или обращение не из рекламы. |
| lead.direction | string | null | Кто начал общение, из которого создан лид: inbound — клиент позвонил или написал сам, outbound — мы позвонили или написали первыми. Ставится один раз и не меняется. |
| lead.click_source | string | null | Рекламная сеть по префиксу параметра: yandex, google, meta, tiktok, vk, microsoft. null, если префикса нет. |
| lead.start_param | string | null | Сырой параметр start ссылки на бота, как пришёл (до 64 символов): yclid_…, промокод, метка кампании. |
| chat | object | null | { id } диалога. |
| actor | object | null | { type, id, name } — кто вызвал системное событие: agent, system, manager. |
| data | object | Поля конкретного события — см. примеры в каталоге. Всегда есть message_id и content — человекочитаемая строка из ленты лида. |
Доставка, повторы, порядок
- Асинхронно, надёжно. Событие попадает в очередь платформы (PostgreSQL) прямо в базе, триггером, вместе с записью самого события; отдельный воркер отправляет его. Ни ответ агента, ни звонок не ждут ни доставки, ни постановки в очередь.
- Повторы. На сетевую ошибку, таймаут,
408,429или5xx— экспоненциальный backoff от 30 секунд до 1 часа, до 8 попыток. На3xxи прочие4xxповтора нет — событие помечается ошибкой сразу (обычно это неверный секрет или URL). - At-least-once. Повтор несёт тот же
event_idи увеличенныйX-SWS-Delivery— дедуплицируйте поevent_id. - Порядок не гарантируется. Сортируйте по
occurred_at; события одного лида связывайте черезlead.id. - Что не приходит. События типов, на которые вы не подписаны; события за время, пока вебхук был на паузе или ещё не подключён; dry-тесты агента из «Превью».
- История последних доставок с HTTP-кодом, числом попыток и ошибкой видна в личном кабинете.
Тестовое событие webhook.test
При «Проверить подключение» платформа отправляет POST с теми же заголовками, но без блоков события. Отвечайте 2xx, ничего не создавая.
{
"event": "webhook.test",
"event_id": "d3c1a2b4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"api_version": "1",
"occurred_at": "2026-09-14T10:00:00.000Z",
"sent_at": "2026-09-14T10:00:00.000Z",
"message": "Тестовое событие от Sales Workshop AI. Вебхук событий подключён."
}Версионирование
api_version фиксирует версию контракта. В рамках версии изменения только аддитивные (новые типы событий, новые необязательные поля) — пишите приёмник устойчиво к ним.
Вопросы и ответы
- Как передать конверсию «написал в мессенджер» в Яндекс Директ?
- Подпишитесь на событие lead.created: оно приходит один раз на обращение и содержит lead.click_id — идентификатор клика из ссылки на бота вида t.me/<бот>?start=yclid_<yclid> (lead.click_source = yandex). Загрузите конверсию в Метрику или Директ по этому id.
- Как узнать, что AI-агент перевёл звонок на менеджера?
- В событии call.ended поле data.termination_reason равно TRANSFERRED, а в блоке lead поле call_resolution равно transferred.
- Что делать, если событие пришло повторно?
- Доставка at-least-once: повтор несёт тот же event_id и увеличенный X-SWS-Delivery. Дедуплицируйте по event_id и отвечайте 2xx.