Webhooks — уведомления о событиях
Webhooks позволяют получать HTTP-уведомления на ваш сервер при наступлении событий: входящие сообщения, исходящие сообщения, изменения статусов и другие.
Как работает
- Вы регистрируете URL вашего сервера через
PATCH /v1/webhooks - WazzaBee отправляет тестовый запрос на ваш URL — если он не вернёт
200, регистрация будет отклонена - При наступлении событий WazzaBee отправляет POST-запрос на ваш URL с данными события
- Каждый запрос подписан HMAC-SHA256 — вы можете проверить подлинность
Два способа подключить webhook
1. Через личный кабинет (просто, без программиста). Откройте «Коннекторы» → «API» → блок «Webhook для входящих». Вставьте адрес вашего сервера и нажмите «Подключить» — кабинет сам проверит адрес и покажет секретный ключ для подписи.
2. Через REST API (для автоматизации). Отправьте запрос PATCH /v1/webhooks — описано ниже.
PATCH /v1/webhooks — регистрация/обновление
URL
PATCH https://api.wazzabee.com/api-gateway/v1/webhooks
Заголовки
Authorization: Bearer ВАШ_API_КЛЮЧ
Content-Type: application/json
Параметры тела запроса
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
webhooksUri | string (URL) | ✅ Да | URL вашего сервера для получения уведомлений. Только https, должен быть доступен из интернета и возвращать 200 на POST-запросы. Не может указывать на локальные или внутренние адреса (localhost, 127.0.0.1, 10.x, 172.16–31.x, 192.168.x, 169.254.x) |
subscriptions | array of strings | Нет | Список событий для подписки. По умолчанию: ["messages_incoming"] |
Доступные подписки (subscriptions)
| Событие | Описание |
|---|---|
messages_incoming | Входящие сообщения от клиентов |
messages_outgoing | Исходящие сообщения (отправленные через API или вручную) |
messages_status | Изменение статуса сообщения (доставлено, прочитано, ошибка) |
contactsAndDealsCreation | Создание новых контактов и сделок |
channelsUpdates | Изменение статуса каналов (подключение, отключение, ошибка) |
Пример запроса
curl -X PATCH "https://api.wazzabee.com/api-gateway/v1/webhooks" \
-H "Authorization: Bearer ВАШ_API_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"webhooksUri": "https://ваш-сервер.com/webhook/wazzabee",
"subscriptions": [
"messages_incoming",
"messages_outgoing",
"messages_status"
]
}'
Успешный ответ (200 OK)
{
"success": true,
"webhooksUri": "https://ваш-сервер.com/webhook/wazzabee",
"subscriptions": [
"messages_incoming",
"messages_outgoing",
"messages_status"
],
"secret": "a1b2c3d4e5f6...64-символьный-hex-ключ",
"message": "Вебхук установлен. Сохраните secret — он используется для подписи уведомлений (HMAC-SHA256)."
}
secret генерируется заново при каждом вызове PATCH. Обязательно сохраните его — он нужен для проверки подписи входящих уведомлений.
Тестовый запрос
При регистрации вебхука WazzaBee сразу отправляет тестовый POST-запрос на ваш URL:
{
"test": true,
"event": "webhook_test",
"challenge": "случайная_строка_для_подтверждения",
"timestamp": "2026-03-12T10:00:00.000Z",
"message": "WazzaBee webhook test — если вы видите это, ваш webhook работает корректно."
}
Ваш сервер должен ответить статусом 200. Если ответ не 200 или URL недоступен — регистрация вебхука будет отклонена с ошибкой.
challenge со случайной строкой. Если ваш сервер вернёт это же значение — в теле ответа или JSON-полем challenge (например {"challenge": "то же значение"}) — webhook получит отметку «подтверждён» (более высокий уровень доверия). Это не обязательно: если ваш приёмник (например готовый no-code сервис) не умеет возвращать значение, webhook всё равно подключится и будет работать под защитой HMAC-подписи.
Возможные ошибки при регистрации
| Код | Ошибка | Причина |
|---|---|---|
400 | Поле webhooksUri обязательно | Не передан URL |
400 | webhooksUri должен быть валидным URL | Формат URL некорректен |
400 | webhooksUri должен использовать https:// | Адрес начинается не с https:// |
400 | webhooksUri не может указывать на локальный или внутренний адрес | URL ведёт на localhost, приватную сеть или metadata-адрес |
400 | Тестовый запрос вернул ошибку N | Ваш сервер вернул не-200 ответ |
400 | Не удалось отправить тестовый запрос | URL недоступен или таймаут (10 секунд) |
GET /v1/webhooks — текущие настройки
URL
GET https://api.wazzabee.com/api-gateway/v1/webhooks
Пример запроса
curl -X GET "https://api.wazzabee.com/api-gateway/v1/webhooks" \
-H "Authorization: Bearer ВАШ_API_КЛЮЧ"
Ответ — вебхук настроен (200 OK)
{
"success": true,
"webhook": {
"webhooksUri": "https://ваш-сервер.com/webhook/wazzabee",
"subscriptions": ["messages_incoming", "messages_outgoing"],
"status": "active",
"failureCount": 0,
"createdAt": "2026-03-12T08:00:00.000Z",
"updatedAt": "2026-03-12T08:00:00.000Z"
}
}
Ответ — вебхук не настроен (200 OK)
{
"success": true,
"webhook": null,
"message": "Вебхук не настроен"
}
Описание полей
| Поле | Тип | Описание |
|---|---|---|
webhooksUri | string | Зарегистрированный URL |
subscriptions | array | Активные подписки |
status | string | active или disabled |
failureCount | integer | Количество неудачных доставок подряд |
createdAt | string (ISO 8601) | Дата создания |
updatedAt | string (ISO 8601) | Дата последнего обновления |
Проверка подписи (HMAC-SHA256)
Каждое уведомление подписано с помощью secret, полученного при регистрации вебхука. Рекомендуем всегда проверять подпись для защиты от поддельных запросов.
Пример проверки на Node.js
const crypto = require("crypto");
function verifyWebhookSignature(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(JSON.stringify(body))
.digest("hex");
return signature === expected;
}
// В обработчике webhook:
app.post("/webhook/wazzabee", (req, res) => {
const signature = req.headers["x-webhook-signature"];
if (!verifyWebhookSignature(req.body, signature, YOUR_WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
// Обработка события...
res.status(200).send("OK");
});
Пример проверки на Python
import hmac
import hashlib
import json
def verify_signature(body: dict, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
json.dumps(body).encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# В обработчике Flask:
@app.route("/webhook/wazzabee", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Webhook-Signature", "")
if not verify_signature(request.json, signature, YOUR_WEBHOOK_SECRET):
return "Invalid signature", 401
# Обработка события...
return "OK", 200
Формат входящего webhook-уведомления
При поступлении входящего сообщения WazzaBee отправляет POST-запрос на ваш URL со следующей структурой:
{
"channel_id": "uuid",
"channel_type": "whatsapp_waba",
"channel_name": "WhatsApp Business",
"conversation_id": "uuid",
"message": {
"id": "msg_id",
"from": "77771234567",
"sender_name": "Клиент",
"type": "text",
"text": "Здравствуйте!",
"timestamp": "1712345678"
},
"operator": {
"crm_user_id": "manager-42",
"crm_user_name": "Алексей Менеджер"
},
"ad": {
"ad_id": "120255518599590051",
"source_type": "ad",
"headline": "Visa4U",
"text": "Отказали в визе? Не все потеряно!",
"media_type": "video",
"image_url": "https://scontent.xx.fbcdn.net/...jpg",
"video_url": "https://video.xx.fbcdn.net/...mp4",
"url": "https://fb.me/2abcXYZ",
"click_id": "AfgHqhgPqgcChoXjlCqLkHfWKeS8"
},
"raw": { ... }
}
Описание полей
| Поле | Тип | Описание |
|---|---|---|
channel_id | string (UUID) | Идентификатор линии связи WazzaBee |
channel_type | string | Тип линии: whatsapp_waba, whatsapp_qr, telegram_bot, telegram_personal |
channel_name | string | Название линии связи |
conversation_id | string (UUID) | null | Идентификатор диалога. Используйте для группировки сообщений одного клиента |
message | object | Данные сообщения |
operator | object | null | Последний оператор, ответивший в этом чате. null если ещё никто не отвечал |
operator.crm_user_id | string | ID пользователя из вашей CRM (переданный при создании iframe-сессии) |
operator.crm_user_name | string | Имя оператора. Берём его из user.name вашей iframe-сессии, а при отправке через API — из поля senderName |
ad | object | отсутствует | Источник обращения: с какого объявления пришёл клиент. Блок приходит только у обращений с рекламы Click-to-WhatsApp на линиях официального WhatsApp (WABA). Описание полей — ниже |
raw | object | Полный оригинальный payload от провайдера (для отладки) |
Блок ad — источник обращения
Если клиент нажал «Написать в WhatsApp» в рекламе Instagram или Facebook, вместе с уведомлением приходит блок ad. У обычных обращений его нет — проверяйте наличие самого поля, а не значения внутри. Блок приходит только на линиях официального WhatsApp: такие данные Meta передаёт лишь по WABA.
| Поле | Тип | Описание |
|---|---|---|
ad_id | string | null | Идентификатор объявления. По нему объявление находится в рекламном кабинете Meta |
source_type | string | null | ad — реклама, post — обычная публикация |
headline | string | null | Заголовок объявления. Может повторяться у разных объявлений — для точной привязки используйте ad_id |
text | string | null | Текст объявления |
media_type | string | null | Тип креатива: image или video |
image_url | string | null | Картинка объявления или кадр из видео |
video_url | string | null | Видео объявления |
url | string | null | Ссылка на объявление |
click_id | string | null | Идентификатор клика по рекламе (ctwa_clid). Сохраните его у себя: по нему в Meta возвращают события о продаже, чтобы реклама оптимизировалась на покупателей |
ad_id и click_id в карточку клиента у себя — тогда в отчётах будет видно, какое объявление приносит сделки, а не просто обращения.
Ответы клиентов на кнопки WABA-шаблонов
Когда вы отправляете клиенту одобренный WABA-шаблон с кнопками Quick Reply (или Interactive buttons / list), и клиент нажимает кнопку — WazzaBee присылает в webhook дополнительные поля message.interactive и message.context. Поле message.text содержит реальный текст нажатой кнопки — его можно показывать в чате CRM как обычное сообщение клиента.
Значения message.type
| Значение | Когда приходит |
|---|---|
text | Обычное текстовое сообщение |
image, video, audio, document, sticker | Медиа |
location, contacts, reaction | Служебные типы WhatsApp |
button | Клик по Quick Reply кнопке WABA-шаблона (старый Meta формат) |
interactive | Клик по Interactive button / list (Meta Cloud API v3) |
Структура поля message.interactive
{
"type": "button" | "button_reply" | "list_reply",
"id": "идентификатор кнопки" | null,
"title": "текст кнопки, который видел клиент" | null,
"description": "описание (только для list_reply)" | null,
"payload": "значение payload (только для type=button)" | null
}
Структура поля message.context
Ссылка на исходное HSM-сообщение, на кнопку которого нажал клиент:
{
"id": "uuid исходящего сообщения в WazzaBee",
"from": "номер отправителя (ваш WABA-номер)",
"gs_id": "внутренний ID сообщения у провайдера",
"meta_msg_id": "wamid.HBgL... — ID в Meta"
}
Пример webhook: клиент нажал кнопку «Да, буду вовремя»
{
"channel_id": "f0e8...",
"channel_type": "whatsapp_waba",
"channel_name": "WhatsApp Business",
"conversation_id": "b1c2...",
"message": {
"id": "wamid.HBgLNzcwMTEwMDEwNTkVAgASGBQzQTY5NkJCNjE3M0JGQjE1MjVEMQA=",
"from": "77011001059",
"sender_name": "Фархат",
"type": "button",
"text": "Да, буду вовремя",
"interactive": {
"type": "button",
"title": "Да, буду вовремя",
"payload": "Да, буду вовремя"
},
"context": {
"id": "584abab5-911a-49ca-a750-95e8116be1e1",
"from": "77079119977",
"gs_id": "378ba4ed-5a5d-4c58-a4e8-b6ca08b2ad7a",
"meta_msg_id": "wamid.HBgL..."
},
"timestamp": "1776870877"
},
"operator": null,
"raw": { ... }
}
message.text теперь содержит реальный текст нажатой кнопки — старые клиенты, которые читают только text, будут видеть в чате осмысленное сообщение «Да, буду вовремя» вместо технической заглушки.
Пример: клик по Interactive list (Meta Cloud API v3)
{
"message": {
"type": "interactive",
"text": "Доставка на дом",
"interactive": {
"type": "list_reply",
"id": "delivery_home",
"title": "Доставка на дом",
"description": "По адресу клиента"
},
"context": { "meta_msg_id": "wamid.HBgL..." }
}
}
Исходящие WABA-шаблоны (messages_outgoing)
Когда вы отправляете HSM-шаблон через API или из UI WazzaBee, при подписке на messages_outgoing в webhook приходит структура с полем template (если шаблон содержит кнопки):
{
"messages": [{
"messageId": "uuid",
"direction": "outgoing",
"type": "template",
"text": "Здравствуйте! Подтвердите встречу завтра в 15:00.",
"template": {
"name": "appointment_reminder_v2",
"language": "ru",
"buttons": [
{ "type": "quick_reply", "text": "Да, буду вовремя" },
{ "type": "quick_reply", "text": "Перенести" }
]
},
"timestamp": "2026-04-22T15:14:42Z"
}]
}
Это позволяет вашей CRM отрисовать кнопки в ленте чата точно так же, как они выглядят у клиента в WhatsApp.
Кто отправил исходящее сообщение (author)
Событие messages_outgoing приходит на любой ответ клиенту: написанный оператором в кабинете WazzaBee, отправленный из чата, встроенного в вашу CRM, посланный шаблоном или через POST /v1/messages/send. Вместе с сообщением приходит блок author — кто именно ответил.
{
"messages": [{
"messageId": "uuid",
"channelId": "uuid",
"chatId": "77001234567",
"contactId": "uuid",
"conversationId": "uuid",
"direction": "outgoing",
"type": "text",
"text": "Здравствуйте! Ваши документы готовы.",
"author": {
"kind": "operator",
"name": "Назерке А.",
"userId": "manager-42"
},
"timestamp": "2026-07-30T09:20:12.000Z"
}]
}
Поля блока author
| Поле | Тип | Описание |
|---|---|---|
author.kind | string | Вид отправки: operator — ответил человек, api — сообщение отправила ваша система, automation — робот или автоответ, broadcast — рассылка |
author.name | string | null | Имя сотрудника. Для ответов из встроенного чата — то самое имя, которое вы передали в user.name при создании iframe-сессии; при отправке по API — senderName. null, если имя не передавали |
author.userId | string | null | Идентификатор сотрудника в вашей CRM (user.id из iframe-сессии или userId при отправке по API). Возвращаем как есть — сопоставлять его с пользователями WazzaBee не нужно |
user.id и user.name при создании iframe-сессии — и имя сотрудника будет и в подписи под сообщением в чате, и в этом вебхуке. Для сообщений, которые ваша система отправляет сама, передавайте senderName и userId в запросе на отправку.
