Вебхук событий: обращения, заявки и звонки AI-агента в вашу аналитику

Три события, по одному на бизнес-факт: клиент обратился впервые (написал в мессенджер или позвонил), заявка передана менеджеру, звонок завершён. На каждое платформа отправляет HTTP POST с JSON на ваш эндпоинт. В каждом событии есть channel (в какой канал написали или позвонили: тип, id и название бота или транка), agent (какой AI-агент обработал), contact и lead, а у обращения из рекламы — lead.click_id. Это отдельная интеграция от webhook-назначения лидов; их можно включить вместе, приёмник у обеих один и тот же по заголовкам и стилю payload.

Подключение

  1. Личный кабинет → Настройки → Интеграции → «Вебхук событий».
  2. Укажите https://-URL, скопируйте сгенерированный секрет, отметьте нужные события.
  3. Нажмите «Проверить подключение» — придёт событие webhook.test, ответьте 2xx.
  4. Сохраните. Дальше события идут автоматически; последние доставки и ошибки видны там же.

Требования к эндпоинту

Каталог событий

Подписка точная: приходят только отмеченные типы; новые типы, которые появятся в платформе позже, не включаются автоматически. Раскройте событие — внутри ровно тот 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 приходит один раз на обращение — в момент первого сообщения клиента в мессенджере (или первого звонка), до ответа агента. Это и есть конверсия «написал в мессенджер»: не нужно дедуплицировать сообщения одного диалога. Чтобы связать её с кликом по объявлению, донесите идентификатор клика до бота.

  1. Ведите рекламу на бота ссылкой с идентификатором клика в параметре 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 символов).
  2. В 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.
  3. Подпишитесь на 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-Typeapplication/json
AuthorizationBearer <секрет> — всегда
X-SWS-Eventтип события, напр. lead.created
X-SWS-Event-IdUUID события — одинаков на всех повторах (ключ дедупликации)
X-SWS-Deliveryномер попытки: 1 для первой, растёт на повторах

Тело запроса

Один JSON-объект: общий конверт + блоки channel, agent, contact, lead, chat, actor и data с полями конкретного события (примеры — в каталоге выше). Скалярные поля всегда присутствуют, null — если значение неизвестно. Блоки contact / lead / channel / agent отражают состояние на момент доставки (обычно секунды после события); сами переходы — в data.

ПолеТипОписание
eventstringТип события из каталога (плюс webhook.test при проверке подключения).
event_idstring (UUID)Идентификатор события. Стабилен на всех повторах доставки — ключ дедупликации.
api_versionstringВерсия контракта. Сейчас "1".
occurred_atstring (ISO-8601, UTC)Когда событие произошло.
sent_atstring (ISO-8601, UTC)Когда отправлена эта попытка доставки.
organization_idnumberID организации в платформе.
demo_modenull | "live"null для реального лида; "live" — тест агента из вкладки «Превью» (dry-тесты не отправляются). У live-превью contact.first_name с префиксом «[Demo] ».
channelobjectКанал события: { type, id, name }. type — telegram, whatsapp_baileys, max, preview, voice; id и name — конкретный бот или транк из настроек каналов.
agentobject | nullAI-агент, который обработал событие: { id, name }. Для нового обращения — агент, назначенный на этот канал.
contactobject | nullid, first_name, last_name, phone, callback_phone, email, telegram, whatsapp, channel_user_id, channel_handle_label, company, role, channel. Синтетические id превью никогда не приходят как телефон.
leadobject | nullid, 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_idstring | nullИдентификатор клика по объявлению из ссылки на бота (см. раздел про рекламные конверсии): значение после префикса yclid_, gclid_, fbclid_, ttclid_, rb_clickid_ или msclkid_. null, если префикса нет или обращение не из рекламы.
lead.directionstring | nullКто начал общение, из которого создан лид: inbound — клиент позвонил или написал сам, outbound — мы позвонили или написали первыми. Ставится один раз и не меняется.
lead.click_sourcestring | nullРекламная сеть по префиксу параметра: yandex, google, meta, tiktok, vk, microsoft. null, если префикса нет.
lead.start_paramstring | nullСырой параметр start ссылки на бота, как пришёл (до 64 символов): yclid_…, промокод, метка кампании.
chatobject | null{ id } диалога.
actorobject | null{ type, id, name } — кто вызвал системное событие: agent, system, manager.
dataobjectПоля конкретного события — см. примеры в каталоге. Всегда есть message_id и content — человекочитаемая строка из ленты лида.

Доставка, повторы, порядок

Тестовое событие 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.