Event webhook: AI agent inquiries, leads and calls in your analytics

Three events, one per business fact: a client got in touch for the first time (messaged in a messenger or called), a lead was handed off to a manager, a call ended. For each one the platform sends an HTTP POST with JSON to your endpoint. Every event includes channel (where the client wrote or called: the type, id and name of the bot or trunk), agent (which AI agent handled it), contact and lead, and an inquiry from an ad also carries lead.click_id. This is a separate integration from the lead webhook destination; you can enable both, and one receiver can serve them — they share the same headers and payload style.

Connecting

  1. Dashboard → Settings → Integrations → “Event webhook”.
  2. Enter an https:// URL, copy the generated secret and tick the events you need.
  3. Click “Test connection” — a webhook.test event arrives; respond with 2xx.
  4. Save. From then on events are sent automatically; recent deliveries and errors are shown in the same place.

Endpoint requirements

Event catalogue

The subscription is exact: only the ticked types are delivered; new types added to the platform later are not enabled automatically. Expand an event to see the exact JSON that will hit your URL.

▸lead.createdNew inquiryLeads

A client messaged you for the first time in a messenger (Telegram, WhatsApp, MAX) or called — a lead was created. One event per inquiry: exactly what you need for a “messaged in a messenger” conversion in Yandex Direct (lead.click_id from the bot link). 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.handoffQualified and handed off to a managerLeads

The AI qualified the client and handed the lead off to a manager (data.reason: ready_to_buy, human_request, …); the contact and lead blocks carry the collected data and qualification.

{
  "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.endedCall endedCalls

The call result: duration, outcome, termination reason, transfer to a manager (lead.call_resolution), summary.

{
  "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
  }
}

Agent replies, individual messages and call utterances, status changes and internal timeline events are not published — the catalogue is extended on request. The call recording link is never sent out.

Ad conversions: “messaged in a messenger” with a click ID

The lead.created event arrives once per inquiry — at the client's first message in a messenger (or first call), before the agent replies. That is exactly the “messaged in a messenger” conversion: no need to deduplicate messages within one conversation. To link it to an ad click, carry the click ID through to the bot.

  1. Point your ads at the bot with a link that carries the click ID in the start parameter: https://t.me/<bot>?start=yclid_<yclid> (in MAX — https://max.ru/<bot>?start=yclid_<yclid>) for Yandex Direct, gclid_… for Google Ads, fbclid_… for Meta, ttclid_… for TikTok, rb_clickid_… for VK. Ad networks only append the click ID to your website's URL, so the usual setup is a redirect: the ad leads to a page on your site, which reads the ID from its URL and redirects to the bot link with it (the parameter allows only letters, digits, _ and -, up to 64 characters).
  2. In Telegram the client's first message arrives as /start yclid_…; in MAX the parameter arrives when the client taps “Start”. The platform stores it on the lead: lead.click_id is the ID itself, lead.click_source is the network derived from the prefix (yandex, google, meta, tiktok, vk), and lead.start_param is the raw value. Without a prefix the platform doesn't guess: a bare number, promo code or campaign tag is stored only in start_param, and click_id stays null. The first click wins: repeated /start from the same lead changes nothing. In MAX the parameter arrives only on the bot's first launch — a client who has messaged the bot before will arrive via the ad link without a click_id.
  3. Subscribe to lead.created, filter on channel.type (telegram, whatsapp_baileys, max — or voice if the first contact was a call) and upload the conversion to your ad account by lead.click_id and occurred_at. Without a click ID, you can still match on contact.phone / contact.telegram through CRM conversion uploads.

How to tell the outcome of a call

A typical partner scenario: the call itself and its recording come from the partner's own telephony, and what they need from the platform is the result — did the agent transfer to a manager, close a deal, qualify the client on its own. Subscribe to lead.created, call.ended and lead.handoff and read:

What you need to knowWhere it is in the events
The client got in touch for the first time (messaged or called), which channel, which agentlead.created: channel.type, channel.name, agent.name, contact, lead.type, lead.click_id
Transferred to a managercall.ended: data.termination_reason = TRANSFERRED; lead.call_resolution = transferred
The client asked for a call-backcall.ended: data.outcome = callback_requested; lead.call_resolution = callback_requested; contact.callback_phone
Qualified / submitted a requestlead.handoff: data.reason (ready_to_buy, human_request, …); contact and lead blocks with qualification
No dealcall.ended: data.outcome ∈ not_interested | wrong_number | hung_up | no_answer and no lead.handoff for this lead.id
Duration, who hung upcall.ended: data.duration_seconds, data.termination_reason (USER_HANGUP, AGENT_HANGUP, …)
Summary in wordslead.summary in call.ended and lead.handoff

Match with your telephony records by contact.phone (the number that called or was dialed) and time: occurred_at of lead.created for the first call, occurred_at and data.duration_seconds of call.ended; data.voice_call_id is the platform's internal ID, not the SIP Call-ID. All events of one lead carry the same lead.id; lead.handoff arrives a few seconds after call.ended.

Authentication

Every request carries an Authorization: Bearer <secret> header. The secret (a UUID) is generated in the dashboard when you connect. Compare it with your own using a constant-time comparison and reject the request on mismatch.

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);
}

Headers

HeaderValue
Content-Typeapplication/json
AuthorizationBearer <secret> — always
X-SWS-Eventevent type, e.g. lead.created
X-SWS-Event-Idevent UUID — the same on every retry (deduplication key)
X-SWS-Deliveryattempt number: 1 for the first, increments on retries

Request body

A single JSON object: a common envelope plus the channel, agent, contact, lead, chat, actor and data blocks with the fields of the specific event (examples are in the catalogue above). Scalar fields are always present, null when the value is unknown. The contact / lead / channel / agent blocks reflect the state at delivery time (usually seconds after the event); the transitions themselves are in data.

FieldTypeDescription
eventstringEvent type from the catalogue (plus webhook.test when testing the connection).
event_idstring (UUID)Event identifier. Stable across all delivery retries — your deduplication key.
api_versionstringContract version. Currently "1".
occurred_atstring (ISO-8601, UTC)When the event happened.
sent_atstring (ISO-8601, UTC)When this delivery attempt was sent.
organization_idnumberOrganization ID in the platform.
demo_modenull | "live"null for a real lead; "live" — an agent test from the Preview tab (dry tests are not sent). In a live preview, contact.first_name is prefixed with “[Demo] ”.
channelobjectThe event's channel: { type, id, name }. type is telegram, whatsapp_baileys, max, preview or voice; id and name identify the specific bot or trunk from your channel settings.
agentobject | nullThe AI agent that handled the event: { id, name }. For a new inquiry — the agent assigned to that channel.
contactobject | nullid, first_name, last_name, phone, callback_phone, email, telegram, whatsapp, channel_user_id, channel_handle_label, company, role, channel. Synthetic preview IDs never arrive as a phone number.
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 fields}, custom_attributes {key: value}.
lead.click_idstring | nullThe ad click ID from the bot link (see the ad conversions section): the value after the yclid_, gclid_, fbclid_, ttclid_, rb_clickid_ or msclkid_ prefix. null if there is no prefix or the inquiry didn't come from an ad.
lead.directionstring | nullWho started the conversation the lead came from: inbound — the client called or wrote in, outbound — we called or wrote first. Set once and never changed.
lead.click_sourcestring | nullThe ad network, derived from the parameter prefix: yandex, google, meta, tiktok, vk, microsoft. null if there is no prefix.
lead.start_paramstring | nullThe raw start parameter of the bot link, as received (up to 64 characters): yclid_…, a promo code, a campaign tag.
chatobject | null{ id } of the conversation.
actorobject | null{ type, id, name } — who triggered a system event: agent, system, manager.
dataobjectFields of the specific event — see the catalogue examples. message_id and content (a human-readable line from the lead timeline) are always present.

Delivery, retries, ordering

The webhook.test event

On “Test connection” the platform sends a POST with the same headers but without event blocks. Respond with 2xx without creating anything.

{
  "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. Вебхук событий подключён."
}

Versioning

api_version pins the contract version. Within a version, changes are additive only (new event types, new optional fields) — build your receiver to tolerate them.

FAQ

How do I send a “messaged in a messenger” conversion to Yandex Direct?
Subscribe to the lead.created event: it arrives once per inquiry and carries lead.click_id — the click ID from a bot link like t.me/<bot>?start=yclid_<yclid> (lead.click_source = yandex). Upload the conversion to Yandex Metrica or Direct by this ID.
How do I know the AI agent transferred the call to a manager?
In the call.ended event, data.termination_reason is TRANSFERRED, and in the lead block call_resolution is transferred.
What if an event arrives twice?
Delivery is at-least-once: a retry carries the same event_id and an incremented X-SWS-Delivery. Deduplicate by event_id and respond with 2xx.