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 у отправителей.
Как это работает
Заголовок раздела «Как это работает»- У каждого аккаунта есть один роутер с токеном вида
R…(префиксR). Публичный URL —https://notifly.ru/router/R<token>/<путь>. - Внешний сервис шлёт
POST(илиGET) с JSON-телом на этот URL. - Роутер прогоняет запрос через все включённые правила. Каждое правило
проверяет две вещи:
- путь URL (
*pathпосле токена) — совпадает ли с паттерном правила; - фильтр — проходит ли JSON-payload (и заголовки) по условиям.
- путь URL (
- Для каждого совпавшего правила рендерятся шаблоны
title/message(Gotext/template) и в канал правила создаётся сообщение. - Запрос целиком сохраняется в историю событий — с заголовками и полным телом, чтобы можно было отладить правила или сгенерировать новое правило из примера.
Публичный эндпоинт
Заголовок раздела «Публичный эндпоинт»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/заголовками (см. ниже) |
| Шаблон заголовка | titleTemplate | Go-шаблон для title сообщения |
| Шаблон текста | messageTemplate | Go-шаблон для message сообщения |
| Markdown | markdown | рендерить сообщение как 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).
Шаблоны title и message
Заголовок раздела «Шаблоны title и message»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).
Подбор правила из события (AI)
Заголовок раздела «Подбор правила из события (AI)»Самый быстрый способ написать правило — взять реальный пример из истории.
Эндпоинт 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.
Примеры маршрутизации
Заголовок раздела «Примеры маршрутизации»Grafana → канал «Критичные» по severity
Заголовок раздела «Grafana → канал «Критичные» по severity»Правило: 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 → разные каналы по типу события
Заголовок раздела «GitHub → разные каналы по типу события»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 → один канал, фильтр по событию
Заголовок раздела «Bitrix24 → один канал, фильтр по событию»Bitrix24 шлёт event в теле. Правило: path = /bitrix, группа
and/payload, условие event contains ONCRMDEAL, канал «CRM».
titleTemplate: Bitrix: {{.Payload.event}}messageTemplate: Сделка {{get "data.FIELDS.ID" .Payload}} обновленаREST API
Заголовок раздела «REST API»Управление роутером и правилами требует 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-инбокс (приём писем по тому же принципу), ассистент и ИИ-квоты.