Webhooks
Для разработчиков

Webhooks

Webhooks — это push-модель: SupportHub сам отправляет HTTP POST-запрос на ваш URL, как только происходит событие. Это удобнее long polling, если у вас есть публичный HTTPS-эндпоинт.

POST/api/v1/webhooks

Зарегистрировать webhook

body
{
  "url": "https://example.com/hooks/supporthub",
  "events": ["ticket.created", "message.created"],
  "description": "Sync to CRM"
}
201 Created
{
  "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"
}
secret возвращается только при создании. Сохраните его — он нужен для проверки подписи входящих запросов.

Пустой массив 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-примеры

billing.payment_received
{
  "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..."
  }
}
billing.plan_renewed / billing.plan_changed
{
  "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"
  }
}
billing.balance_low
{
  "type": "billing.balance_low",
  "data": {
    "plan": "pro",
    "balance": 12.50,
    "price": 99.0,
    "shortfall": 86.50
  }
}

Структура payload

POST body
{
  "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).

GET/api/v1/webhooks

Список зарегистрированных webhooks

Возвращает все webhooks workspace (без секрета). Поле consecutive_failures показывает счётчик подряд провальных доставок — если equals 5, webhook был авто-деактивирован, нужно поднять PATCH'ем (см. ниже).

PATCH/api/v1/webhooks/{webhook_id}

Изменить webhook (re-activate, edit url/events)

Частичное обновление существующей подписки. Передавайте только те поля что хотите изменить. Самый частый кейс — реактивация после auto-deactivate (поправили эндпоинт у себя → шлёте {"is_active": true}).

body
{
  "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 стартует с чистого листа, не упадёт сразу обратно если первая доставка после реактивации тоже отвалится.
DELETE/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)

server.py
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)

server.mjs
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-го раза».
Была ли страница полезной?