Webhooks
Webhooks — это push-модель: SupportHub сам отправляет HTTP POST-запрос на ваш URL, как только происходит событие. Это удобнее long polling, если у вас есть публичный HTTPS-эндпоинт.
/api/v1/webhooksЗарегистрировать webhook
{
"url": "https://example.com/hooks/supporthub",
"events": ["ticket.created", "message.created"],
"description": "Sync to CRM"
}{
"id": "9c...",
"url": "https://example.com/hooks/supporthub",
"events": ["ticket.created", "message.created"],
"secret": "Vd9q...long-random...",
"is_active": true,
"description": "Sync to CRM",
"created_at": "2026-04-06T10:00:00"
}Пустой массив events = подписка на все типы. Иначе доставляются только перечисленные события.
Типы событий
| Событие | Когда срабатывает |
|---|---|
| ticket.created | Новый тикет в воркспейсе |
| ticket.updated | Изменены поля (статус, тема, теги, приоритет, отдел) |
| ticket.assigned | Тикет взят оператором (первое назначение) |
| ticket.transferred | Передача operator → operator |
| ticket.force_taken | Супервайзер забрал тикет у текущего оператора |
| ticket.closed | Тикет закрыт |
| ticket.reopened | Закрытый тикет открыт заново |
| ticket.rated | Посетитель оставил CSAT-оценку |
| message.created | Новое сообщение в любом канале |
| message.edited | Сообщение отредактировано |
| message.deleted | Сообщение удалено |
| message.reaction.added | Реакция добавлена |
| message.reaction.removed | Реакция убрана |
| contact.created | Новый контакт через виджет / identify() |
| contact.identified | Контакт привязан к email/имени/телефону |
| operator.status.changed | Оператор online/away/offline |
| billing.payment_received | Оплата прошла (Heleket / FreeKassa) — баланс зачислен |
| billing.payment_failed | Платёж отклонён или отменён в gateway |
| billing.plan_renewed | Тариф продлён списанием с баланса (auto-renew) |
| billing.plan_changed | Сменён тариф на другой платный (через next_plan) |
| billing.plan_downgraded | Авто-понижение до starter (нет средств / истёк период) |
| billing.balance_low | Предупреждение: баланса не хватит на следующее списание |
| billing.subscription_expired | Подписка истекла без продления |
Billing payload-примеры
{
"id": "8c4f1a...",
"type": "billing.payment_received",
"timestamp": "2026-05-07T10:00:00+00:00",
"workspace_id": "27744f73-...",
"data": {
"payment_id": "p1...",
"order_id": "ord_42",
"amount": 99.0,
"currency": "USD",
"method": "heleket",
"transaction_id": "txid_...",
"new_balance": 199.0
},
"_links": {
"payments": "/api/v1/workspaces/27744f73-.../payments",
"payment": "/api/v1/payments/p1..."
}
}{
"type": "billing.plan_renewed",
"data": {
"plan": "pro",
"amount": 99.0,
"new_balance": 100.0,
"expires_at": "2026-06-07T10:00:00+00:00"
}
}
{
"type": "billing.plan_changed",
"data": {
"old_plan": "pro",
"new_plan": "team",
"expires_at": "2026-06-07T10:00:00+00:00"
}
}{
"type": "billing.balance_low",
"data": {
"plan": "pro",
"balance": 12.50,
"price": 99.0,
"shortfall": 86.50
}
}Структура payload
{
"id": "8c4f1a...", // event UUID — стабилен при retry
"type": "ticket.created",
"timestamp": "2026-05-07T10:00:00+00:00",
"workspace_id": "27744f73-...",
"data": { ... }, // зависит от типа события
"_links": { // относительные URL для GET-дозапросов
"ticket": "/api/v1/tickets/aa.../",
"messages": "/api/v1/tickets/aa.../messages",
"history": "/api/v1/tickets/aa.../history"
}
}data содержит минимум для дедупликации/роутинга на вашей стороне. Если нужны полные объекты — дёрните URL из _links через ваш API key (см. /api/v1/auth).
/api/v1/webhooksСписок зарегистрированных webhooks
Возвращает все webhooks workspace (без секрета). Поле consecutive_failures показывает счётчик подряд провальных доставок — если equals 5, webhook был авто-деактивирован, нужно поднять PATCH'ем (см. ниже).
/api/v1/webhooks/{webhook_id}Изменить webhook (re-activate, edit url/events)
Частичное обновление существующей подписки. Передавайте только те поля что хотите изменить. Самый частый кейс — реактивация после auto-deactivate (поправили эндпоинт у себя → шлёте {"is_active": true}).
{
"url": "https://example.com/hooks/v2", // optional
"events": ["ticket.created"], // optional
"description": "Updated subscription", // optional
"is_active": true // optional
}is_active=true также сбрасывает consecutive_failures в 0 — webhook стартует с чистого листа, не упадёт сразу обратно если первая доставка после реактивации тоже отвалится./api/v1/webhooks/{webhook_id}Удалить webhook
Возвращает 204 No Content. Полностью удаляет подписку из БД (вместе с историей доставок через CASCADE).
Подпись HMAC
Каждый запрос подписан HMAC-SHA256 по телу запроса с использованием секрета webhook. Подпись передаётся в заголовке:
X-Webhook-Signature: sha256=<hex digest>
X-Webhook-Event: ticket.created
X-Webhook-Id: 9c...Проверка подписи (Python / Flask)
import hmac, hashlib
from flask import Flask, request, abort
SECRET = b"Vd9q...long-random..."
app = Flask(__name__)
@app.post("/hooks/supporthub")
def hook():
sig = request.headers.get("X-Webhook-Signature", "")
expected = "sha256=" + hmac.new(
SECRET, request.get_data(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
event = request.headers["X-Webhook-Event"]
payload = request.get_json()
print(event, payload)
return "", 204Проверка подписи (Node.js / Express)
import express from "express";
import crypto from "node:crypto";
const SECRET = "Vd9q...long-random...";
const app = express();
app.post(
"/hooks/supporthub",
express.raw({ type: "application/json" }),
(req, res) => {
const expected =
"sha256=" +
crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const sig = req.header("X-Webhook-Signature") || "";
const ok =
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
const event = req.header("X-Webhook-Event");
const payload = JSON.parse(req.body.toString());
console.log(event, payload);
res.sendStatus(204);
}
);
app.listen(3000);Retry-политика
- Успех = HTTP-код
2xx, ответ должен прийти в течение 10 секунд. - При ошибке (любой код < 200 или ≥ 300, таймаут, сетевая ошибка) выполняется до 6 попыток с экспоненциальной задержкой: сразу, +30s, +2m, +10m, +1h, +6h.
- Если все 6 попыток упали — доставка помечается как
failed, счётчикconsecutive_failuresна webhook'e инкрементится. После 5 подряд провальных доставок webhook автоматически деактивируется (is_active = false) — реактивация через PATCH (TODO: эндпоинт в roadmap, пока через админку). Любая успешная доставка сбрасывает счётчик в 0. - Заголовок
X-Webhook-Id— UUID конкретной доставки (одна и та же на всех попытках). Полеidв payload — UUID самого события (тоже стабильно при retry). Используйте любое из них для дедупликации на своей стороне. - Заголовок
X-Webhook-Attempt— номер попытки (1..6). Полезно для диагностики если ваш receiver хочет логировать «эта доставка пришла со 2-го раза».