Перейти к содержимому

Webhook-роутер

Webhook-роутер — это единая публичная точка приёма вебхуков. Любой внешний сервис (GitHub, GitLab, Grafana, Bitrix24, Stripe, ваш CI и т. д.) шлёт POST на один и тот же URL вида /router/R<token>/<путь>, а Notifly сам разбирает payload и раскладывает его по каналам — по правилам, которые вы настраиваете в админке.

Это замена старого «webhook»-механизма: вместо отдельной ссылки на каждый интегрируемый сервис у вас есть один роутер на аккаунт (singleton), а маршрутизацию задают правила. Один входящий запрос может совпасть сразу с несколькими правилами и улететь в несколько каналов.

Удобно для:

  • разведения алертов Grafana по командам (/grafana → разные каналы по alert.severity);
  • маршрутизации событий GitHub (push в один канал, issues в другой);
  • приёма вебхуков Bitrix24, CRM, платёжек без написания бэкенда;
  • единой «воронки» вебхуков, которую потом можно пересортировать правилами, не меняя URL у отправителей.
  1. У каждого аккаунта есть один роутер с токеном вида R… (префикс R). Публичный URL — https://notifly.ru/router/R<token>/<путь>.
  2. Внешний сервис шлёт POST (или GET) с JSON-телом на этот URL.
  3. Роутер прогоняет запрос через все включённые правила. Каждое правило проверяет две вещи:
    • путь URL (*path после токена) — совпадает ли с паттерном правила;
    • фильтр — проходит ли JSON-payload (и заголовки) по условиям.
  4. Для каждого совпавшего правила рендерятся шаблоны title/message (Go text/template) и в канал правила создаётся сообщение.
  5. Запрос целиком сохраняется в историю событий — с заголовками и полным телом, чтобы можно было отладить правила или сгенерировать новое правило из примера.
POST|GET https://notifly.ru/router/R<token>/<любой/путь>
Content-Type: application/json
  • Поддерживаются методы POST и GET.
  • Тело должно быть JSON-объектом. Лимит размера — 256 КБ; всё, что больше, обрезается (в событии выставляется payloadTruncated: true).
  • Если тело — не валидный JSON, событие сохраняется со статусом error (invalid JSON payload), правила не оцениваются.
  • Авторизация не нужна — секретом служит сам токен R… в URL.
  • Ответ всегда 200 с телом вида {"accepted": true, "matched": 2}, где matched — число сработавших правил. Если роутер выключен — {"accepted": false, "error": "router disabled"}.

Каждый входящий запрос — это одно событие квоты типа «webhook-роутер» (пересылка в несколько каналов отдельно не тарифицируется). При исчерпании дневной квоты роутер вернёт {"accepted": false, "error": "daily event quota exceeded"} и пришлёт вам уведомление.

Пример: пингуем роутер вручную.

Окно терминала
curl -X POST "https://notifly.ru/router/RAbcDef123/grafana" \
-H "Content-Type: application/json" \
-d '{"status":"firing","alert":{"severity":"critical"},"message":"disk full"}'
# {"accepted":true,"matched":1}

Правило (WebhookRouterRule) состоит из:

ПолеJSON-ключСмысл
КаналappIdв какой канал слать (App или Client — любой канал аккаунта)
Имяnameчеловекочитаемое название правила
Путьpathпаттерн пути URL (см. ниже); пусто или / — любой путь
Фильтрfilterусловия над payload/заголовками (см. ниже)
Шаблон заголовкаtitleTemplateGo-шаблон для title сообщения
Шаблон текстаmessageTemplateGo-шаблон для message сообщения
Markdownmarkdownрендерить сообщение как Markdown
Приоритетpriorityприоритет создаваемого сообщения
Включеноenabledучаствует ли правило в маршрутизации (по умолчанию true)
Позицияpositionпорядок в списке (для UI)

Все включённые правила проверяются независимо — совпавших может быть несколько, и тогда сообщение уйдёт в каждый из их каналов.

Паттерн path использует Gin-синтаксис сегментов:

  • /литерал — точное совпадение сегмента (/github, /grafana/prod);
  • /:param — один любой сегмент, его значение доступно в шаблоне как {{.Path.Params.param}};
  • /*wildcard — «хвост» пути (только в конце), доступен как {{.Path.Params.wildcard}}.

Правило без пути (пусто или /) совпадение по пути не проверяет вовсе — оно работает на любом входящем пути, остаётся только фильтр. Лишние сегменты в запросе не совпадают с литеральным паттерном.

Примеры (отправитель шлёт на /router/R<token>/<путь>):

path правилаСовпадает с путём запроса
/github/github
/grafana/prod/grafana/prod
/teams/:team/teams/backend (тогда {{.Path.Params.team}} = backend)
/hooks/*rest/hooks/ci/build/42 (тогда {{.Path.Params.rest}} = ci/build/42)
/ или пустосрабатывает на любой запрос (только по фильтру)

filter — это набор групп условий. Между группами действует логическое И: запрос проходит фильтр, только если прошли все группы. Пустой фильтр (null или без групп) совпадает всегда.

Группа (RouterConditionGroup):

{
"type": "and",
"target": "payload",
"conditions": [
{"path": "alert.severity", "op": "eq", "value": "critical"}
]
}
  • target — откуда брать данные: payload (тело JSON, по умолчанию) или headers (HTTP-заголовки запроса).
  • type — логика внутри группы:
typeСрабатывает, когда…
andсовпали все условия группы
anyсовпало хотя бы одно условие
and_notни одно условие не совпало (NOT any)
any_notхотя бы одно условие НЕ совпало (NOT all)

Условие (RouterCondition) — это path, op и value:

  • path — dotted-path до поля. Поддерживаются вложенность и индексы массивов: alert.severity, items.0.name.
  • op — оператор:
opЗначение
eqполе существует и его строковое значение равно value
neqполе отсутствует или его значение не равно value
containsзначение содержит value (регистронезависимо)
existsполе присутствует (value можно не указывать)
regexзначение поля матчится регулярным выражением value
gtчисловое значение поля больше value
ltчисловое значение поля меньше value

Сравнение eq/neq/contains/regex идёт по строковому представлению значения (число 5"5", true"true"). Для gt/lt значение поля приводится к числу.

Для заголовков target: "headers" тот же синтаксис, но path — имя заголовка (например X-GitHub-Event).

titleTemplate и messageTemplate — это строки на Go text/template. Если шаблон пуст или статичен (без {{), он используется как есть. Если после рендера заголовок или текст пусты, подставляется «умный» fallback из самого payload (берётся первое осмысленное поле вроде title/message/ status, иначе сводка верхнеуровневых полей или путь запроса).

Доступный контекст:

ВыражениеЧто даёт
{{.Payload.status}}поле верхнего уровня из тела JSON
{{.Payload.alert.severity}}вложенное поле
{{.Headers.X-GitHub-Event}}значение HTTP-заголовка
{{.Path.Full}}полный путь запроса (после токена)
{{.Path.Params.team}}значение :param/*wildcard из пути
{{.ReceivedAt}}время приёма запроса

Доступные функции:

ФункцияПример
upper{{upper .Payload.level}}
lower{{lower .Payload.level}}
default{{default "unknown" .Payload.status}}
json{{json .Payload}} — значение как JSON-строка
get{{get "alert.text" .Payload}} — доступ по dotted-path

Пример шаблонов:

titleTemplate: Grafana: {{upper .Payload.alert.severity}}
messageTemplate: {{.Payload.message}} (статус: {{default "—" .Payload.status}})

Включите markdown: true, чтобы текст рендерился как Markdown в клиентах.

Каждый входящий запрос сохраняется как событие (WebhookRouterEvent) с полным контекстом: method, path, remoteIp, все headers, сырое тело payloadRaw, флаг payloadTruncated, список сработавших правил matchedRuleIds, созданных сообщений createdMessageIds и статус:

  • matched — совпало хотя бы одно правило;
  • unmatched — ни одно правило не сработало;
  • error — тело не распарсилось как JSON.

Историю можно фильтровать по статусу (status), подстроке (q — ищет по телу/заголовкам), диапазону времени (from/to в RFC3339) и листать курсором (cursor, limit — по умолчанию 50, максимум 100).

Самый быстрый способ написать правило — взять реальный пример из истории. Эндпоинт POST /webhook-router/rule/from-event/:eid по сохранённому событию возвращает черновик правила:

{
"path": "/grafana",
"suggested": [
{"type": "and", "target": "payload",
"conditions": [{"path": "alert.severity", "op": "eq", "value": "critical"}]}
],
"titleTemplate": "Alert: {{.Payload.status}}",
"messageTemplate": "",
"paths": ["status", "alert", "alert.severity", "message"]
}
  • Без тела (или с пустым prompt) работает эвристика без обращения к ИИ: берёт «дискриминирующие» поля (event/type/action/status/severity…) и полезные заголовки (X-…-Event), плюс отдаёт paths — все dotted-path-и из payload, удобные для ручной сборки условий.
  • С телом {"prompt": "слать только критичные алерты в #ops"} подключается ИИ-ассистент: он понимает текстовое пожелание и собирает условия и шаблоны под него (списывается один ИИ-запрос из дневной квоты; см. Ассистент и квоты). Если ИИ не настроен или вернул ошибку — автоматически откатывается к эвристике.

Результат — это черновик: проверьте и при необходимости поправьте его перед сохранением через POST /webhook-router/rule.

Правило: path = /grafana, фильтр — одна группа and/payload с условием alert.severity eq critical, канал — «Критичные».

Окно терминала
curl -X POST "https://notifly.ru/router/R<token>/grafana" \
-H "Content-Type: application/json" \
-d '{"status":"firing","alert":{"severity":"critical"},"message":"OOM on db-1"}'

GitHub кладёт тип события в заголовок X-GitHub-Event. Заводим два правила с одним путём /github, но разными фильтрами на заголовок:

  • Правило «Push»: группа and/headers, условие X-GitHub-Event eq push, канал «Деплои».
  • Правило «Issues»: группа and/headers, условие X-GitHub-Event eq issues, канал «Задачи».
titleTemplate: GitHub {{.Headers.X-GitHub-Event}}
messageTemplate: {{get "repository.full_name" .Payload}}: {{.Payload.action}}

Bitrix24 шлёт event в теле. Правило: path = /bitrix, группа and/payload, условие event contains ONCRMDEAL, канал «CRM».

titleTemplate: Bitrix: {{.Payload.event}}
messageTemplate: Сделка {{get "data.FIELDS.ID" .Payload}} обновлена

Управление роутером и правилами требует client-токена (C…), MCP-кода с правом записи (M…) или Basic Auth. Все эндпоинты ниже — относительно базового URL (https://notifly.ru), заголовок авторизации — X-Notifly-Key. Изменяющие операции недоступны по «расшаренному» доступу.

Публичный приём вебхуков (/router/...) авторизации не требует — секрет в самом токене.

МетодПутьНазначение
GET/webhook-routerполучить роутер (создаётся при первом обращении), вместе с правилами
PUT/webhook-routerобновить роутер (name, enabled)
POST/webhook-router/rotate-tokenсгенерировать новый токен R… (старый URL перестанет работать)
GET/webhook-router/ruleсписок правил
POST/webhook-router/ruleсоздать правило
PUT/webhook-router/rule/:ridобновить правило
DELETE/webhook-router/rule/:ridудалить правило
GET/webhook-router/eventистория событий (q, status, from, to, cursor, limit)
GET/webhook-router/event/:eidодно событие целиком (заголовки + тело)
DELETE/webhook-router/event/:eidудалить событие из истории
POST/webhook-router/rule/from-event/:eidсгенерировать черновик правила из события (эвристика или ИИ по prompt)
GET/webhook-router/messageсообщения, созданные роутером (q — фильтр по тексту)
DELETE/webhook-router/message/:midудалить созданное роутером сообщение
POST|GET/router/:token/*pathпубличный приём вебхука (без авторизации)

Пример: создать правило через API.

Окно терминала
curl -X POST "$NOTIFLY_URL/webhook-router/rule" \
-H "X-Notifly-Key: C<client-token>" \
-H "Content-Type: application/json" \
-d '{
"appId": 12,
"name": "Grafana critical",
"path": "/grafana",
"filter": {"groups": [
{"type": "and", "target": "payload", "conditions": [
{"path": "alert.severity", "op": "eq", "value": "critical"}
]}
]},
"titleTemplate": "Grafana: {{upper .Payload.alert.severity}}",
"messageTemplate": "{{.Payload.message}}",
"markdown": true,
"priority": 8
}'

См. также: push-сообщения, каналы и токены, email-инбокс (приём писем по тому же принципу), ассистент и ИИ-квоты.