Webhooks — входящие и исходящие интеграции

Руководство по webhook-интеграциям: входящие события, исходящие уведомления, HMAC подпись.

Обзор Webhook-системы

Trigly поддерживает два типа webhooks:

  1. Исходящие (Outgoing) — Trigly отправляет HTTP-запросы на ваш сервер при наступлении событий (отправка, доставка, открытие, клик, баунс)
  2. Входящие (Incoming) — провайдеры каналов отправляют события в Trigly (баунсы email, статусы SMS, сообщения Telegram)

Архитектура:

┌─────────────┐     Исходящие      ┌──────────────┐
│   Trigly     │ ──────────────────→│  Ваш сервер  │
│   Backend    │                    │  (webhook)   │
└──────┬──────┘                    └──────────────┘
       │
       │     Входящие
       │
┌──────┴──────┐
│ Провайдеры  │
│ (Unisender, │
│  Telegram,  │
│  SMS.ru)    │
└─────────────┘

Исходящие Webhooks

Создание webhook

Создайте webhook для получения уведомлений о событиях ваших кампаний:

curl -X POST https://api.trigly.ru/api/v1/campaigns/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://myapp.ru/webhooks/trigly",
    "event_types": [
      "message.sent",
      "message.delivered",
      "message.opened",
      "message.clicked",
      "message.bounced",
      "message.failed",
      "campaign.started",
      "campaign.completed"
    ],
    "secret": "whsec_a1b2c3d4e5f6g7h8i9j0",
    "is_active": true
  }'

Ответ:

{
  "id": "webhook-uuid",
  "url": "https://myapp.ru/webhooks/trigly",
  "event_types": ["message.sent", "message.delivered", "..."],
  "is_active": true,
  "failure_count": 0,
  "created_at": "2026-03-20T10:00:00Z"
}

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

Событие Описание Когда срабатывает
message.sent Сообщение отправлено После передачи провайдеру
message.delivered Сообщение доставлено Подтверждение от провайдера
message.opened Сообщение открыто Загрузка пикселя / отчёт провайдера
message.clicked Клик по ссылке Переход по отслеживаемой ссылке
message.bounced Баунс (отскок) Hard/soft bounce от МТА
message.failed Ошибка отправки Невозможно отправить (после ретраев)
campaign.started Кампания запущена Смена статуса на running
campaign.completed Кампания завершена Все сообщения обработаны

Формат payload

Trigly отправляет POST-запрос с JSON-телом:

{
  "event": "message.delivered",
  "timestamp": "2026-03-20T10:05:30Z",
  "data": {
    "message_id": "msg-uuid",
    "campaign_id": "campaign-uuid",
    "customer_id": "customer-uuid",
    "customer_email": "ivan@example.com",
    "channel": "email",
    "status": "delivered",
    "external_id": "provider-message-id",
    "metadata": {
      "provider": "smtp",
      "ip": "1.2.3.4"
    }
  }
}

Для события message.clicked дополнительно передаётся:

{
  "data": {
    "...": "...",
    "url": "https://myshop.ru/products/123",
    "tracking_id": "track-uuid"
  }
}

HTTP-заголовки

Каждый webhook-запрос содержит следующие заголовки:

Заголовок Описание
Content-Type application/json
X-Trigly-Event Тип события (e.g. message.delivered)
X-Trigly-Signature HMAC-SHA256 подпись тела запроса
X-Trigly-Timestamp Unix timestamp запроса
X-Trigly-Delivery-Id Уникальный ID доставки (для идемпотентности)
User-Agent Trigly-Webhook/1.0

HMAC подпись

Trigly подписывает каждый webhook-запрос с использованием HMAC-SHA256 и вашего секрета. Это позволяет убедиться, что запрос действительно пришёл от Trigly, а не от злоумышленника.

Алгоритм формирования подписи:

  1. Берётся тело запроса (raw bytes)
  2. Вычисляется HMAC-SHA256 с ключом = ваш secret
  3. Результат кодируется в hex

Проверка подписи на вашем сервере:

Python

import hmac
import hashlib

def verify_trigly_webhook(body: bytes, signature: str, secret: str) -> bool:
    """Проверка HMAC-SHA256 подписи webhook от Trigly."""
    expected = hmac.new(
        secret.encode('utf-8'),
        body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)


# В вашем обработчике (FastAPI пример):
from fastapi import Request, HTTPException

@app.post("/webhooks/trigly")
async def handle_trigly_webhook(request: Request):
    body = await request.body()
    signature = request.headers.get("X-Trigly-Signature", "")

    if not verify_trigly_webhook(body, signature, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")

    payload = await request.json()
    event = payload["event"]
    data = payload["data"]

    if event == "message.delivered":
        # Обновить статус в вашей системе
        update_delivery_status(data["customer_email"], "delivered")
    elif event == "message.bounced":
        # Пометить email как невалидный
        mark_email_invalid(data["customer_email"])

    return {"ok": True}

JavaScript (Node.js)

const crypto = require('crypto');

function verifyTriglyWebhook(body, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Express пример:
app.post('/webhooks/trigly', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-trigly-signature'];

  if (!verifyTriglyWebhook(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const payload = JSON.parse(req.body);
  console.log(`Event: ${payload.event}`, payload.data);

  res.json({ ok: true });
});

PHP

<?php
function verifyTriglyWebhook($body, $signature, $secret) {
    $expected = hash_hmac('sha256', $body, $secret);
    return hash_equals($expected, $signature);
}

$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TRIGLY_SIGNATURE'] ?? '';

if (!verifyTriglyWebhook($body, $signature, WEBHOOK_SECRET)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

$payload = json_decode($body, true);
// Обработка события...

echo json_encode(['ok' => true]);
?>

Ruby

require 'openssl'

def verify_trigly_webhook(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest('sha256', secret, body)
  Rack::Utils.secure_compare(expected, signature)
end

Повторные попытки (Retries)

Если ваш сервер отвечает HTTP-кодом >= 400 или не отвечает в течение 10 секунд, Trigly повторит запрос:

Попытка Задержка
1-я Немедленно
2-я 30 секунд
3-я 5 минут

После 3 неудачных попыток webhook помечается как неактивный (failure_count увеличивается). При достижении 10 последовательных ошибок webhook автоматически деактивируется.

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

Каждый запрос содержит уникальный X-Trigly-Delivery-Id. Используйте его для дедупликации на вашей стороне:

# Проверка идемпотентности
delivery_id = request.headers.get("X-Trigly-Delivery-Id")
if redis.sismember("processed_webhooks", delivery_id):
    return {"ok": True}  # Уже обработано

redis.sadd("processed_webhooks", delivery_id)
redis.expire("processed_webhooks", 86400)  # TTL 24 часа

# Обработка...

Управление webhooks

# Список webhooks
curl https://api.trigly.ru/api/v1/campaigns/webhooks \
  -H "Authorization: Bearer $TOKEN"

# Обновление
curl -X PATCH https://api.trigly.ru/api/v1/campaigns/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"event_types": ["message.sent", "message.delivered"]}'

# Удаление
curl -X DELETE https://api.trigly.ru/api/v1/campaigns/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer $TOKEN"

# Получение по ID
curl https://api.trigly.ru/api/v1/campaigns/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer $TOKEN"

Входящие Webhooks (от провайдеров)

Trigly принимает webhook-уведомления от провайдеров каналов для обновления статусов доставки, обработки баунсов и приёма входящих сообщений.

Все входящие webhooks публичные — не требуют JWT-авторизации. Они доступны по префиксу /hooks/.

Email: Unisender

POST /hooks/email/unisender

Принимает callback-события от Unisender API:

Событие Действие Trigly
bounce Помечает сообщение как bounced
complaint Добавляет email в список подавления
unsubscribe Устанавливает is_unsubscribed = true

Настройка в Unisender:

  1. Перейдите в настройки → Webhooks
  2. Укажите URL: https://api.trigly.ru/hooks/email/unisender
  3. Выберите типы событий: Bounce, Complaint, Unsubscribe

Email: универсальный обработчик баунсов

POST /hooks/email/bounce

Универсальный эндпоинт для обработки баунсов от любого провайдера:

{
  "email": "invalid@example.com",
  "bounce_type": "hard_bounce",
  "reason": "550 User not found",
  "message_id": "msg-uuid"
}
Тип баунса Действие
hard_bounce Добавление в SuppressionList
soft_bounce Логирование, повторная попытка
spam Автоматическая отписка контакта

Telegram

POST /hooks/telegram/{org_id}

Принимает обновления (updates) от Telegram Bot API. Trigly автоматически устанавливает этот URL как webhook при подключении Telegram-бота через /api/v1/channels/config.

Обрабатываемые события:

Тип обновления Действие
/start с deep link Привязка telegram_chat_id к контакту через HMAC-токен
/start без параметра Приветственное сообщение от бота
callback_query Логирование события в ClickHouse, ответ answerCallbackQuery
Текстовое сообщение Логирование в ClickHouse как входящее событие

Deep link для привязки Telegram:

Trigly генерирует уникальные ссылки для привязки Telegram-аккаунта к контакту:

https://t.me/your_bot?start=BASE64_TOKEN

Токен содержит: org_id:customer_id:timestamp, подписанный HMAC-SHA256. Срок действия — 24 часа.

Получение ссылки:

curl -X POST https://api.trigly.ru/api/v1/channels/telegram-link \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "CUSTOMER_UUID"}'

# {"link": "https://t.me/my_bot?start=abc123...", "expires_at": "2026-03-21T10:00:00Z"}

SMS.ru

POST /hooks/sms/status

Принимает callback-уведомления о статусе доставки SMS от sms.ru:

Код статуса Значение Действие
103 Доставлено Обновление статуса на delivered
104 Не доставлено Помечается как failed
105 Ошибка оператора Помечается как failed, планируется retry

Настройка в sms.ru:

  1. Личный кабинет → Настройки → HTTP-уведомления
  2. URL: https://api.trigly.ru/hooks/sms/status
  3. Включить уведомления о доставке

Примеры интеграций

Интеграция с CRM (amoCRM/Bitrix24)

Используйте исходящие webhooks для синхронизации статусов в CRM:

@app.post("/webhooks/trigly")
async def handle_trigly(request: Request):
    payload = await request.json()

    if payload["event"] == "message.opened":
        # Обновить сделку в amoCRM
        customer_email = payload["data"]["customer_email"]
        await amocrm.update_lead(
            email=customer_email,
            custom_field="email_opened",
            value=True
        )

    elif payload["event"] == "message.clicked":
        # Создать задачу менеджеру
        await amocrm.create_task(
            email=customer_email,
            text=f"Клиент перешёл по ссылке: {payload['data']['url']}"
        )

    return {"ok": True}

Интеграция с аналитикой

Отправка событий в собственную систему аналитики:

app.post('/webhooks/trigly', async (req, res) => {
  const { event, data, timestamp } = req.body;

  // Отправка в ClickHouse / BigQuery / Amplitude
  await analyticsClient.track({
    event: `trigly.${event}`,
    userId: data.customer_id,
    properties: {
      campaign_id: data.campaign_id,
      channel: data.channel,
      timestamp,
    }
  });

  res.json({ ok: true });
});

Интеграция с Telegram-ботом

Обработка входящих сообщений через webhook:

# Trigly автоматически логирует все входящие сообщения в ClickHouse.
# Вы можете использовать эти данные для аналитики:

# Получение хронологии контакта (включая входящие TG-сообщения)
curl https://api.trigly.ru/api/v1/cdp/customers/CUSTOMER_ID/timeline \
  -H "Authorization: Bearer $TOKEN"

Отладка и мониторинг

Просмотр ошибок доставки webhooks

curl https://api.trigly.ru/api/v1/campaigns/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer $TOKEN"

# Ответ включает failure_count и is_active

Логи в ClickHouse

Все события доставки (включая webhook-результаты) записываются в таблицу delivery_events в ClickHouse. Анализируйте их через API аналитики каналов:

# Ошибки за последние 7 дней
curl "https://api.trigly.ru/api/v1/channels/analytics/errors?period=7d" \
  -H "Authorization: Bearer $TOKEN"

# Воронка доставки
curl "https://api.trigly.ru/api/v1/channels/analytics/funnel?period=30d" \
  -H "Authorization: Bearer $TOKEN"

Тестирование webhook-эндпоинтов

Для тестирования используйте сервисы вроде webhook.site или ngrok:

# 1. Запустите ngrok
ngrok http 3000

# 2. Создайте webhook с ngrok-URL
curl -X POST https://api.trigly.ru/api/v1/campaigns/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/trigly",
    "event_types": ["message.sent", "message.delivered"],
    "secret": "test_secret"
  }'

# 3. Запустите кампанию и наблюдайте запросы в терминале ngrok

Безопасность

Рекомендации

  1. Всегда проверяйте HMAC-подпись — не обрабатывайте запросы без валидации
  2. Используйте HTTPS для URL webhook — Trigly не отправляет webhooks на HTTP
  3. Не храните секрет в коде — используйте переменные окружения
  4. Реализуйте идемпотентность — один и тот же webhook может быть отправлен дважды
  5. Отвечайте быстро — обрабатывайте webhook асинхронно, сразу отвечая 200 OK
  6. Ротируйте секреты — периодически обновляйте webhook secret через PATCH-запрос

Пример асинхронной обработки

from fastapi import BackgroundTasks

@app.post("/webhooks/trigly")
async def handle_webhook(request: Request, background_tasks: BackgroundTasks):
    body = await request.body()
    # Проверка подписи
    verify_signature(body, request.headers)

    payload = json.loads(body)

    # Ставим в очередь — отвечаем сразу
    background_tasks.add_task(process_webhook_event, payload)

    return {"ok": True}


async def process_webhook_event(payload):
    """Асинхронная обработка — может занимать время."""
    event = payload["event"]
    data = payload["data"]

    if event == "message.bounced":
        await update_crm(data)
        await send_alert(data)
        await log_to_analytics(data)

IP-фильтрация

Trigly отправляет webhook-запросы с фиксированного набора IP-адресов. Для дополнительной безопасности вы можете настроить whitelist:

# nginx конфигурация
location /webhooks/trigly {
    allow 185.x.x.x;  # IP Trigly (уточняйте в поддержке)
    deny all;
    proxy_pass http://backend:3000;
}

Актуальный список IP-адресов доступен по запросу в поддержку: hello@trigly.ru или @trigly_support.

Не нашли ответ?

Swagger UI с интерактивной документацией и поддержка в Telegram.