Webhooks — входящие и исходящие интеграции
Руководство по webhook-интеграциям: входящие события, исходящие уведомления, HMAC подпись.
Обзор Webhook-системы
Trigly поддерживает два типа webhooks:
- Исходящие (Outgoing) — Trigly отправляет HTTP-запросы на ваш сервер при наступлении событий (отправка, доставка, открытие, клик, баунс)
- Входящие (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, а не от злоумышленника.
Алгоритм формирования подписи:
- Берётся тело запроса (raw bytes)
- Вычисляется HMAC-SHA256 с ключом = ваш
secret - Результат кодируется в 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:
- Перейдите в настройки → Webhooks
- Укажите URL:
https://api.trigly.ru/hooks/email/unisender - Выберите типы событий: 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:
- Личный кабинет → Настройки → HTTP-уведомления
- URL:
https://api.trigly.ru/hooks/sms/status - Включить уведомления о доставке
Примеры интеграций
Интеграция с 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
Безопасность
Рекомендации
- Всегда проверяйте HMAC-подпись — не обрабатывайте запросы без валидации
- Используйте HTTPS для URL webhook — Trigly не отправляет webhooks на HTTP
- Не храните секрет в коде — используйте переменные окружения
- Реализуйте идемпотентность — один и тот же webhook может быть отправлен дважды
- Отвечайте быстро — обрабатывайте webhook асинхронно, сразу отвечая 200 OK
- Ротируйте секреты — периодически обновляйте 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.