Вебхуки

Мы сами присылаем запрос, когда в проекте что-то происходит.

Зачем

Без вебхуков интеграция вынуждена опрашивать нас по таймеру: «не появилось ли нового сообщения?». Это медленно, тратит лимит запросов и всё равно отстаёт на длину интервала. Вебхук приходит сразу.

Как подписаться

В кабинете: Настройки → API и MCP → Вебхуки → Добавить вебхук. Укажите адрес и отметьте события.

Через API (так делают коннекторы):

curl -X POST "https://api.tygy.ru/v1/webhooks" \
  -H "Authorization: Bearer $TYGY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Синхронизация с CRM",
    "url": "https://example.com/tygy-webhook",
    "events": ["message.created", "conversation.status_changed"]
  }'

В ответе придёт ключ подписи. Он показывается один раз — сохраните его.

Адрес должен начинаться с https://. Исключение — http://localhost, чтобы можно было отлаживаться локально.

События

СобытиеКогда приходит
conversation.createdПоявился новый диалог
conversation.status_changedДиалог открыли, закрыли, отложили или пометили спамом
conversation.assignedДиалог назначили на оператора
conversation.tags_changedУ диалога изменились теги
message.createdНовое сообщение — входящее, исходящее или внутренняя заметка
contact.createdПоявился новый контакт
contact.updatedКонтакт изменился
organization.createdПоявилась новая компания клиента

Событие приходит независимо от того, кто сделал изменение: оператор в кабинете, клиент в канале, сценарий или ваша же интеграция через API.

Что приходит

POST с телом:

{
  "id": "evt_9f2a1c8e40b1f2a4b6d9d3e7",
  "type": "message.created",
  "created_at": "2026-09-12T10:00:00.000Z",
  "api_version": "v1",
  "company_id": "ckx1…",
  "data": { "id": "m1", "conversation_id": "c1", "body": "Здравствуйте", "…": "…" }
}

Внутри dataтот же объект, который вернул бы соответствующий метод чтения. Дополнительный запрос за подробностями не нужен.

Заголовки:

ЗаголовокЗначение
X-Webhook-IdИдентификатор события, совпадает с id в теле
X-Webhook-EventТип события
X-Webhook-TimestampВремя отправки, unix-секунды
X-Webhook-SignatureПодпись, см. ниже
X-Webhook-AttemptНомер попытки, например 1/6

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

Подпись — это HMAC-SHA256 от строки «время.тело» с вашим ключом подписи. Проверяйте её на каждом запросе: без проверки любой может прислать вам поддельное событие.

Возьмите сырое тело запроса, до разбора JSON. Пересобранный из объекта JSON даст другие байты и подпись не сойдётся.

const crypto = require('node:crypto');

function verify(rawBody, headers, secret) {
  const timestamp = headers['x-webhook-timestamp'];
  const signature = headers['x-webhook-signature'];
  // Не принимайте старые запросы — это защита от повторной отправки.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected =
    'v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signature ?? '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hashlib, hmac, time

def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
    timestamp = headers.get("x-webhook-timestamp", "")
    signature = headers.get("x-webhook-signature", "")
    if abs(time.time() - int(timestamp or 0)) > 300:
        return False
    expected = "v1=" + hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
function verify(string $rawBody, array $headers, string $secret): bool {
    $timestamp = $headers['x-webhook-timestamp'] ?? '';
    $signature = $headers['x-webhook-signature'] ?? '';
    if (abs(time() - (int) $timestamp) > 300) return false;
    $expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($expected, $signature);
}

Что отвечать

Ответьте любым кодом 2xx как можно быстрее — у вас 5 секунд. Если обработка долгая, положите событие в свою очередь и ответьте сразу, а разбирайте потом.

Повторы

Если ваш адрес не ответил или ответил не 2xx, мы повторим:

ПопыткаЧерез
21 минуту
35 минут
430 минут
52 часа
612 часов

Итого шесть попыток примерно за пятнадцать часов.

Ответ 410 Gone останавливает подписку сразу — так вы сообщаете, что адрес больше не нужен.

Если десять событий подряд исчерпают все попытки, мы остановим подписку и пришлём уведомление владельцу и администраторам проекта. Молча умерший вебхук хуже, чем честно остановленный. Починив адрес, включите подписку в кабинете — счётчик обнулится.

Журнал

В кабинете у каждой подписки есть журнал отправок за 7 дней: код ответа, длительность, текст ошибки, номер попытки. Через API то же самое доступно методом GET /v1/webhooks/{id}/deliveries.

Кнопка «Тест» отправляет пробное событие webhook.test — удобно проверить, что адрес вообще принимает запросы.

Порядок и повторная доставка

Порядок событий не гарантирован: при повторах позднее событие может прийти раньше. Ориентируйтесь на created_at внутри тела.

Одно и то же событие может прийти дважды — например, если ваш ответ не дошёл до нас. Запоминайте id события и не обрабатывайте его повторно.