Для розробників
Вебхуки Beta
Отримуйте надійні сповіщення з цифровим підписом, коли змінюються контакти, розмови, повідомлення, теги або склад команди.
Відкрити довідник APIСтворіть ендпоінт
- 01
Відкрийте Налаштування, виберіть Для розробників, знайдіть розділ Вебхуки і натисніть Додати ендпоінт.
- 02
Заповніть поле Назва, введіть публічну HTTPS-адресу в полі URL ендпоінта, а потім виберіть принаймні один пункт у розділі Події.
- 03
Натисніть Додати ендпоінт. Також його можна створити через
POST /api/v1/webhooks.
Sonny відхиляє URL з обліковими даними, localhost, приватні та link-local діапазони IP, а також DNS-імена, що вказують на непублічну адресу.
Одразу збережіть секрет підпису
Він починається з whsec_ і показується лише один раз. Скопіюйте його в менеджер секретів, перш ніж натиснути Збережено.
Обмежте ендпоінт вибраними каналами
У розділі Налаштування → Для розробників кожен ендпоінт може отримувати події з усіх каналів або лише з вибраних — як під час додавання, так і під час подальшого редагування. Ендпоінти, створені до появи обмеження за каналами, отримують події з усіх каналів, доки ви їх не зміните.
Клієнти API та MCP задають такий самий фільтр через sourceIds під час створення або оновлення ендпоінта. Порожній масив означає всі джерела робочого простору. Якщо ID вказано, події розмов і повідомлень доставляються лише тоді, коли розмова належить до одного з вибраних джерел. Події рівня робочого простору, як-от зміни контактів або учасників, на ендпоінт з фільтром за джерелами не надсилаються.
{
"name": "Product A agent",
"url": "https://agent.example.com/sonny",
"events": ["conversation.created", "message.created"],
"sourceIds": ["cm_source_id"]
}Не доставляйте те, що ваша інтеграція все одно відкидає
Агент, що відповідає через API, «прокидається» від власної відповіді, якщо не вказати інакше. Три необов’язкові фільтри звужують те, що отримує ендпоінт. Усі вони типово вимкнені, тож наявні ендпоінти працюють як раніше.
agentGroupIds- Лише розмови, що належать указаним групам агентів. Фільтр враховує, кому належить розмова, а не канал, яким вона надійшла, тож розмова зі спільних вхідних потрапить до групи, яка нею володіє. Розмови без групи не доставляються, а оскільки групу зазвичай призначають уже після початку розмови,
conversation.createdчасто спрацьовує ще до того, як з’являється група для зіставлення. customerMessagesOnly- Лише повідомлення, написані клієнтами. Пропускає відповіді вашої команди, внутрішні нотатки та все, що надіслано через API, MCP або AI-відповідача, — зокрема власні відповіді цього ендпоінта.
excludedSenderIds- Пропускає повідомлення від указаних колег. Створіть для інтеграції окремий обліковий запис колеги й виключіть його — так вона не «прокидатиметься» від власних відповідей, але й надалі знатиме, коли розмову перебирає людина. Саме цей сигнал потрібен AI-агенту, щоб відійти вбік.
Фільтри повідомлень застосовуються лише до подій message.*; усім іншим, як і раніше, керують типи подій. Відфільтрована подія відкидається ще до створення доставки, тож нічого вам не коштує й ніколи не відображається як збій. Корисні дані та apiVersion у будь-якому разі не змінюються.
Об’єднуйте серію повідомлень в одну доставку
Споживач, який під час «пробудження» читає всю гілку, нічого не виграє від чотирьох окремих доставок за десять секунд. Задайте coalesceSeconds — і повідомлення однієї розмови збиратимуться протягом цього часу, а потім надійдуть однією доставкою. Типово це вимкнено: ендпоінти без цього параметра й далі отримують кожне повідомлення окремо.
Згрупована доставка надходить як conversation.activity з розмовою та всіма зібраними ID повідомлень, тож читайте гілку один раз, а не для кожного повідомлення. Це єдиний параметр, що змінює структуру отримуваних даних, тому його потрібно вмикати явно. Інші фільтри теж діють — повідомлення, яке вони виключили, ніколи не потрапляє до групи. Кожна розмова має власне вікно, а повторні спроби розглядають групу як одну доставку.
{
"type": "conversation.activity",
"data": {
"conversationId": "cm_conversation_id",
"messageIds": ["cm_first_message", "cm_second_message"]
}
}Ендпоінти з автентифікацією через bearer-токен або API-ключ
Якщо ваш приймач розташований за шлюзом, який вимагає заголовок, додайте його в розділі Власні заголовки під час створення або редагування ендпоінта чи передайте headers через API. Значення шифруються під час зберігання й ніколи не повертаються — збережений заголовок повертається лише з назвою, а якщо надіслати саму назву, збережене значення залишиться. Щоб вилучити всі заголовки, надішліть порожній масив.
Якщо перенести ендпоінт на інший хост, збережені значення більше не використовуються: їх доведеться ввести знову, щоб облікові дані ніколи не потрапили туди, для чого їх не видавали. Якщо змінити лише шлях, значення збережуться.
Заголовки самого Sonny мають пріоритет над вашими, тож власний заголовок ніколи не замінить sonny-signature, content-type чи заголовки ідентифікації доставки. Де можливо, перевіряйте підпис: він автентифікує кожен запит, а статичний токен лише ідентифікує того, хто звертається.
{
"name": "Gateway",
"url": "https://api.example.com/hooks/sonny",
"events": ["conversation.created"],
"headers": [{ "name": "Authorization", "value": "Bearer …" }]
}Безпека й надійність
- Підпис необробленого тіла запиту
- HMAC-SHA256 охоплює Unix-мітку часу, крапку та незмінене тіло запиту в UTF-8.
- Захист від повторного відтворення
- Відхиляйте мітки часу, що відстають від поточного часу або випереджають його більш ніж на п’ять хвилин, навіть якщо HMAC дійсний.
- Надійні повторні спроби
- Відповіді з кодом, відмінним від 2xx, повторюються через 1m, 5m, 30m, 2h, 6h. Спроба 6 — остання.
Контракт запиту
sonny-signature- t=<unix-seconds>,v1=<sha256-hex>
sonny-event- Тип події — для швидкої маршрутизації.
sonny-delivery-id- Стабільний ID для ідемпотентності та звернень у підтримку.
user-agent- Sonny-Webhooks/1.0
{
"id": "cm_event_id",
"type": "message.created",
"apiVersion": "2026-07-15",
"createdAt": "2026-07-15T12:00:00.000Z",
"data": {
"conversationId": "cm_conversation_id",
"messageId": "cm_message_id"
}
}Перевірте підпис
Спершу прочитайте необроблене тіло запиту. Якщо розібрати JSON і серіалізувати його знову, зміняться пробіли, і дійсний підпис не пройде перевірку.
import { createHmac, timingSafeEqual } from "node:crypto";
const rawBody = await request.text(); // do not parse/re-serialize first
const signature = request.headers.get("sonny-signature");
const secret = process.env.SONNY_WEBHOOK_SECRET;
if (!signature || !secret) throw new Error("Missing webhook signature or secret");
const { t, v1 } = Object.fromEntries(signature.split(",").map(p => p.split("=")));
if (!/^\d+$/.test(t) || !/^[a-f0-9]{64}$/i.test(v1)) {
throw new Error("Malformed signature");
}
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) {
throw new Error("Stale webhook");
}
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
if (!timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"))) {
throw new Error("Invalid signature");
}Каталог подій
webhook.heartbeatНадсилається кожні п’ять хвилин увімкненим підписникам (зокрема *), навіть без нових повідомлень. Використовує звичайний шлях підписаної доставки й повторних спроб та ігнорує фільтри розмов. Налаштуйте сповіщення про відсутні чи застарілі heartbeat-події; сама подія не перевіряє розмови, що очікують на відповідь.
{"intervalSeconds":300,"nextExpectedAt":"2026-09-29T16:05:00.000Z"}contact.createdКонтакт створено.
{"contactId":"cm_contact_id"}contact.updatedКонтакт оновлено або об’єднано.
{"contactId":"cm_contact_id"}contact.property_updatedЗмінилося значення властивості контакту — його встановив колега, API або форма. Надсилається лише за справжньої зміни (вибір значення, яке вже встановлено, нічого не надсилає) і містить попереднє значення; null означає, що значення не задано. Значення, заповнені віджетом сайту (identify), цю подію не надсилають. Новий контакт, який отримує значення за замовчуванням властивості типу «Список», також надсилає цю подію з попереднім значенням null.
{"contactId":"cm_contact_id","propertyId":"cm_property_id","propertyName":"Stage","previousValue":"Prospect","value":"Qualified"}contact.deletedКонтакт архівовано або об’єднано з іншим контактом. Його все ще можна прочитати з archived=true.
{"contactId":"cm_contact_id"}contact.erasedКонтакт остаточно стерто (наприклад, на запит про стирання даних за GDPR) разом з усіма його розмовами, повідомленнями й вкладеннями. Його більше не можна прочитати; видаліть усі збережені вами копії.
{"contactId":"cm_contact_id"}conversation.createdРозмову створено.
{"conversationId":"cm_conversation_id"}conversation.updatedРозмова змінилася.
{"conversationId":"cm_conversation_id"}conversation.closedРозмову закрито.
{"conversationId":"cm_conversation_id"}conversation.deletedРозмову переміщено в Кошик, і вона більше не доступна через публічний API.
{"conversationId":"cm_conversation_id"}message.createdСтворено повідомлення або внутрішню нотатку. Контекст, ім’я, адреса ел. пошти та номер телефону контакту і короткий попередній перегляд повідомлення додаються, коли доступні; текст внутрішніх нотаток ніколи не включається.
{"conversationId":"cm_conversation_id","messageId":"cm_message_id","context":{"sourceId":"cm_source_id","sourceName":"Shopstar Go","storeName":"Shiney Store","inboxId":"cm_inbox_id","inboxName":"Go support","agentGroupId":"cm_group_id","agentGroupName":"Support","identityVerified":true,"status":"open","lastRepliedAt":"2026-09-25T09:00:00.000Z"},"contact":{"id":"cm_contact_id","email":"shiney@example.com","phone":"27821234567","name":"Shiney"},"message":{"id":"cm_message_id","type":"incoming","senderType":"human","senderId":null,"senderName":"Shiney","textPreview":"Could you check my bill?","textTruncated":false}}message.updatedНадіслану відповідь відредаговано в історії розмови. Уже доставлені листи не змінюються.
{"conversationId":"cm_conversation_id","messageId":"cm_message_id"}tag.createdТег створено.
{"tagId":"cm_tag_id"}tag.updatedТег оновлено.
{"tagId":"cm_tag_id"}tag.deletedТег видалено.
{"tagId":"cm_tag_id"}member.invitedЗапрошено учасника робочого простору.
{"invitationId":"cm_invitation_id"}member.updatedЗмінилася роль, статус, доступ до каналів, ім’я, посада або налаштування призначення учасника.
{"memberId":"cm_membership_id"}member.removedУчасника видалено.
{"memberId":"cm_membership_id"}invitation.acceptedЗапрошення до робочого простору прийнято.
{"invitationId":"cm_invitation_id","memberId":"cm_membership_id"}invitation.cancelledОчікуване запрошення до робочого простору скасовано.
{"invitationId":"cm_invitation_id"}conversation.activityПовідомлення однієї розмови, згруповані в одну доставку. Містить знімок кожного повідомлення, якщо він доступний. Надсилається замість message.created ендпоінтам із вікном групування; підписатися на цю подію напряму не можна.
{"conversationId":"cm_conversation_id","messageIds":["cm_message_id","cm_other_message_id"]}webhook.testТестова подія, запитана адміністратором.
{"message":"This is a test webhook from Sonny."}
Як працює доставка
- Поверніть будь-який статус 2xx протягом 10 секунд, щоб доставку було позначено як успішну.
- Робіть обробники ідемпотентними. Використовуйте
idподії абоsonny-delivery-id, щоб ігнорувати дублікати. - Переспрямування не виконуються. Натомість оновіть URL ендпоінта в Sonny.
- Розмір тіла відповіді обмежено: для діагностики зберігаються лише перші 4 КіБ.
- Натисніть Тест, щоб надіслати подію
webhook.test. Натисніть Доставки, щоб переглянути 50 останніх подій. Якщо доставка остаточно завершилася збоєм, натисніть Повторити, щоб надіслати її знову.
Як помітити, що потік подій замовк
Додайте webhook.heartbeat до подій свого ендпоінта в налаштуваннях «Для розробників» або через update_webhook. Активні підписники (зокрема *) отримують підписаний сигнал активності (heartbeat) кожні п’ять хвилин, навіть коли нових повідомлень немає. Сигнали активності ігнорують фільтри каналів, команд і повідомлень та не містять даних розмов. Вони проходять тим самим шляхом доставки й мають ті самі повторні спроби, що й повідомлення. Перевіряйте createdAt і nextExpectedAt, щоб стара повторна спроба не виглядала як свіжий сигнал; перш ніж надсилати сповіщення про збій, зробіть поправку на затримки опитування й мережі. Черга доставок може затримувати сигнали активності або зовсім їх блокувати.
Використовуйте list_webhook_deliveries з дозволом webhooks:read, щоб переглядати спроби та збої. get_status перевіряє з’єднання з базою даних, а не доставку вебхуків. Окремо запускайте за розкладом list_conversations з параметрами status=open, awaitingReply=true, sort=waitingSince і direction=asc, щоб знаходити давні гілки без відповіді, навіть коли потік мовчить.
Увімкніть «Компактні дані» в налаштуваннях «Для розробників» або встановіть compact=true для ендпоінта, щоб пропускати контекстні мітки, але зберігати ID маршрутизації, відправника, час і 200-символьний уривок повідомлення. Це стосується й згрупованих доставок; текст внутрішніх нотаток, як і раніше, не включається. Типові корисні дані не змінюються, окрім доданої мітки часу повідомлення. Відредагуйте області дії наявного API-ключа, щоб надати webhooks:read і contacts:read для журналів доставки та прямого пошуку контактів. Обидві області дії потребують ключа з доступом до всіх каналів і доступу учасника до всіх каналів; ключ з обмеженою областю не можна розширити, лише додавши дозволи.
Пов’язані посібники
- Публічний APIBeta
Сортуйте й синхронізуйте розмови, діліться контекстом клієнтів, читайте звіти й надсилайте файли за допомогою API-ключів з обмеженими правами.
- Вхідні
Статуси, пріоритети, призначення, відкладення, масові дії та клавіатурні скорочення.
- Контакти
Як у Sonny створюються контакти, як ними керувати, позначати тегами й об’єднувати.
- Команда та ролі
Запрошуйте користувачів, створюйте команди й розберіться, який доступ мають власник, адміністратор, агент і спостерігач.