Для розробників

Вебхуки Beta

Отримуйте надійні сповіщення з цифровим підписом, коли змінюються контакти, розмови, повідомлення, теги або склад команди.

Відкрити довідник API

Створіть ендпоінт

  1. 01

    Відкрийте Налаштування, виберіть Для розробників, знайдіть розділ Вебхуки і натисніть Додати ендпоінт.

  2. 02

    Заповніть поле Назва, введіть публічну HTTPS-адресу в полі URL ендпоінта, а потім виберіть принаймні один пункт у розділі Події.

  3. 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 для журналів доставки та прямого пошуку контактів. Обидві області дії потребують ключа з доступом до всіх каналів і доступу учасника до всіх каналів; ключ з обмеженою областю не можна розширити, лише додавши дозволи.

Пов’язані посібники