API — webhooks и события

Что это

Webhooks — механизм уведомления вашей системы о событиях в кабинете в реальном времени. Вместо того чтобы периодически опрашивать API, вы регистрируете URL — и MCM шлёт POST-запрос на него при каждом событии.

Подписка

Через UI: /integrations → раздел «Webhooks» → форма «URL + события» → «+».

Через API:

POST /api/v1/webhooks
Authorization: Bearer mcm_...
Content-Type: application/json

{
  "url": "https://crm.example.com/mcm-hook",
  "events": ["call.end", "callback.requested"]
}

Ответ:

{
  "ok": true,
  "id": 7,
  "secret": "1f2c3d...",
  "note": "Сохраните secret — больше он не будет показан."
}

Этот secret используется для проверки подписи каждого пуша.

Формат пуша

POST https://crm.example.com/mcm-hook
Content-Type: application/json
X-MCM-Event: call.end
X-MCM-Signature: sha256=<hex>
User-Agent: mcm-webhook/1.0

{
  "event": "call.end",
  "tenant_id": "uuid",
  "data": { ... },
  "ts": "2026-05-01T22:30:00+00:00"
}

Проверка подписи

Python:

import hmac, hashlib

def verify(secret, body, header_signature):
    expected = 'sha256=' + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_signature)

# в обработчике:
sig = request.headers['X-MCM-Signature']
if not verify(SECRET, request.body, sig):
    return Response(403)

PHP:

function verifyMcmWebhook(string $secret, string $body, string $sig): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
    return hash_equals($expected, $sig);
}

Поддерживаемые события

call.start Начало звонка
call.end Завершение звонка
call.answered Ответ на звонок
call.missed Пропущенный звонок
chat.new Новый чат с сайта
chat.message Новое сообщение в чате
callback.requested Заказ обратного звонка из коллтрекинга/виджета
transcript.ready Транскрипция готова
* Все события

Структура data по событиям

Конверт (event, tenant_id, ts) одинаков для всех событий, различается
только содержимое data. Ниже — состав полей и типы.

Звонки: call.start, call.answered, call.missed, call.end

Общие поля для всех четырёх событий:

Поле Тип Описание
call_id string Идентификатор звонка целиком. Одинаков у всех событий одного звонка — используйте как ключ идемпотентности
linkedid string Синоним call_id
uniqueid string Идентификатор первого плеча звонка
direction string inbound — входящий, outbound — исходящий, internal — внутренний
from string Номер звонящего, только цифры
to string Номер вызываемого, только цифры
did string Ваш номер, на который поступил входящий. У исходящих пусто
caller_name string Имя звонящего, если передано оператором. Может быть пустым
extension string Внутренний номер сотрудника, участвовавшего в разговоре: у исходящих — кто звонил, у входящих — кто ответил. Пусто, если разговора с сотрудником не было (например, вызов завершился в IVR)
started_at string Начало звонка, ISO 8601 с часовым поясом
tenant_domain string Домен вашего кабинета

Дополнительные поля отдельных событий:

Событие Поле Тип Описание
call.answered answered_at string Момент ответа, ISO 8601
call.missed status string no_answer, busy или failed
ring_time int Сколько секунд шёл вызов до отбоя
call.end status string answered, no_answer, busy, failed
answered bool Состоялся ли разговор
duration int Секунды от начала вызова до отбоя
talk_time int Секунды разговора. У неотвеченных 0
recording_url string | null Ссылка на запись. null, если записи нет. Открывается после входа в кабинет

Пример call.end:

{
  "event": "call.end",
  "tenant_id": "3f7a1c92-0b64-4e18-9c25-7a1d5e8b04f6",
  "ts": "2026-08-31T12:50:40+03:00",
  "data": {
    "call_id": "1750000000.12345",
    "linkedid": "1750000000.12345",
    "uniqueid": "1750000000.12345",
    "direction": "outbound",
    "from": "74951234567",
    "to": "79001234567",
    "did": "",
    "caller_name": "",
    "extension": "205",
    "started_at": "2026-08-31T12:49:50+03:00",
    "tenant_domain": "mcm000.smart.mcm.ru",
    "status": "answered",
    "answered": true,
    "duration": 27,
    "talk_time": 10,
    "recording_url": "https://mcm000.smart.mcm.ru/statistic/recording/1750000000.12345"
  }
}

Важно про время отправки. События call.start и call.answered отправляются
после завершения разговора, а не в момент самого события: данные о звонке фиксируются при его отбое.
Метки времени внутри (started_at, answered_at) верные, поэтому для журналирования
звонков в CRM этого достаточно. Для всплывающей карточки в момент входящего вызова webhooks не подходят —
используйте интеграции с CRM, где это реализовано отдельно.

chat.new — новый чат с сайта

Поле Тип Описание
session_id string Идентификатор чата, ключ идемпотентности
token string Токен сессии посетителя
visitor_name string Имя, если посетитель представился
visitor_phone string Телефон, если оставлен
visitor_email string Почта, если оставлена
page_url string Страница, с которой начат чат, вместе с UTM-метками
referrer string Откуда посетитель пришёл на страницу
visitor_ip string IP-адрес посетителя
started_at string Начало чата, ISO 8601

chat.message — новое сообщение в чате

Поле Тип Описание
message_id string Идентификатор сообщения, ключ идемпотентности
session_id string К какому чату относится
from string visitor или operator
text string Текст сообщения
attachment_url string | null Ссылка на вложение
attachment_name string | null Имя файла вложения
created_at string Время сообщения, ISO 8601
visitor_name string Имя посетителя из чата
visitor_phone string Телефон посетителя из чата

Служебные заметки операторов в webhooks не попадают.

callback.requested — заказ обратного звонка

Поле Тип Описание
callback_id string Идентификатор заявки, ключ идемпотентности
session_id string | null Чат, из которого заказан звонок
visitor_name string Имя, оставленное в форме
phone string Номер для обратного звонка
status string Состояние обработки заявки
notes string Комментарий к заявке
created_at string Время заказа, ISO 8601

transcript.ready — транскрипция готова

Поле Тип Описание
transcript_id string Идентификатор транскрипции, ключ идемпотентности
call_id string Звонок, к которому относится. Совпадает с call_id звонковых событий
uniqueid string Идентификатор плеча звонка
call_date string Дата и время звонка, ISO 8601
duration int Длительность записи в секундах
text string Расшифровка с разделением на реплики оператора и клиента
sentiment string | null Тональность разговора, если определялась
keywords string | null Ключевые слова, если выделялись
completed_at string Когда расшифровка была готова, ISO 8601
recording_url string | null Ссылка на запись разговора

Поля sentiment и keywords заполняются не всегда — обрабатывайте null.

Retry policy

Если ваш сервер вернул статус не 2xx или не ответил — MCM повторяет доставку:

  • Попытка 1 — сразу
  • Попытка 2 — через 60 секунд
  • Попытка 3 — через 5 минут
  • Попытка 4 — через 30 минут
  • Попытка 5 — через 2 часа

После 5 неудачных попыток событие помечается delivered_at = now() с last_error = "GIVE UP" и больше не отправляется. Статус последней попытки виден в /integrations.

Идемпотентность

Каждое событие имеет уникальный data.linkedid (для звонков) или data.session_id (для чатов). Используйте это поле как идемпотентность-ключ — при повторной доставке (retry) одного и того же события данные те же.

Типичные интеграции

  • amoCRM/Bitrix24 — подпишитесь на call.end, callback.requested. По call.end создавайте сделку/задачу; по callback.requested — лид.
  • Slack/Telegram — на call.missed шлите сообщение оператору с номером и временем.
  • Google Sheets / BI — на call.end добавляйте строку в таблицу для аналитики.