Что отправляет webhook
Webhook передаёт во внешнюю систему каждое новое уведомление, которое появляется в панели ноды после включения интеграции
Он подходит для собственного обработчика, системы мониторинга, автоматизации или промежуточного сервиса, который отправляет сообщения в нужный мессенджер
Webhook не передаёт письма, вложения, пароли почтовых аккаунтов и содержимое почтовых ящиков
Текст уведомления и details могут содержать домен, адрес почтового ящика, IP сервера или hostname источника импорта. Считайте payload конфиденциальными операционными данными
Как подключить endpoint
После сохранения панель показывает только hostname endpoint. Полный URL повторно не возвращается и остаётся пустым в поле редактирования
Ключ подписи после сохранения тоже не возвращается. Чтобы оставить текущий endpoint и ключ без изменений, сохраните форму с пустыми полями. При замене URL обязательно создайте новый отличающийся ключ. Чтобы заменить только ключ, заполните его поле. Чтобы отключить доставку, выключите переключатель
- 1
Войдите в панель с ролью владельца
- 2
Откройте «Сервер» → «Настройки системы»
- 3
Включите «Webhook уведомлений»
- 4
Укажите полный HTTPS URL обработчика
- 5
Сгенерируйте ключ подписи и скопируйте его в конфигурацию обработчика до сохранения формы
- 6
Сохраните настройки
- 7
Создайте контролируемое уведомление и проверьте журнал запросов своего обработчика
Требования к endpoint
Перед каждым соединением система заново разрешает DNS-имя и подключается только к публичному IP. Это правило защищает ноду от перенаправления запросов во внутреннюю сеть через DNS rebinding
Path и query string разрешены для маршрутизации, но не должны содержать пароли, токены и другие секреты. Подлинность запроса подтверждает отдельная HMAC-подпись
| Требование | Правило |
|---|---|
| Схема | Только https://
|
| Длина | Не более 2048 байт |
| Адрес | Абсолютный URL с публичным hostname или публичным IP |
| Порт | Стандартный 443 или явно указанный порт от 1 до 65535 |
| Запрещённые узлы | localhost, домены .localhost, .local, .internal, loopback, private, link-local, multicast, CGNAT, служебные, тестовые и NAT64 IP
|
| Userinfo | Конструкция user:password@host запрещена
|
| Fragment | Часть после # запрещена
|
| Redirect | Переходы по ответам 3xx не выполняются |
HTTP-запрос
| Параметр | Значение |
|---|---|
| Метод | POST
|
| Content-Type | application/json
|
| User-Agent | svoimail-webhook/1
|
| X-Svoimail-Webhook-Version | 1
|
| X-Svoimail-Event-ID | То же значение, что в поле event_id
|
| X-Svoimail-Webhook-Timestamp | Unix-время формирования попытки доставки в секундах |
| X-Svoimail-Webhook-Signature | Подпись вида v1={hex}, где hex — HMAC-SHA-256 в нижнем регистре
|
| TLS | Версия 1.2 или новее |
| Таймаут | 10 секунд на одну попытку |
| Максимальный размер заголовков ответа | 64 КБ |
| Максимальный читаемый ответ | 64 КБ, содержимое ответа не используется |
Проверка подписи запроса
Точное подписываемое значение: v1\n{unix_timestamp}\n{event_id}\n{raw_body}. Последняя часть — исходные байты HTTP-тела без завершающего перевода строки, если его не было в запросе
Ключ в панели представлен 43 символами base64url без padding и после декодирования содержит ровно 32 байта. Для HMAC используйте декодированные байты, а не текст из поля
Каждая повторная попытка получает новый timestamp и подпись, но сохраняет прежний event_id. Ограничение времени защищает от позднего replay, а уникальность event_id — от повторного выполнения принятого события
- 1
Получите исходные байты тела без предварительного разбора JSON и преобразования кодировки
- 2
Проверьте, что
X-Svoimail-Webhook-Timestampсодержит целое Unix-время и отличается от текущего времени не более чем на 5 минут - 3
Составьте подписываемое значение из
v1, timestamp, event ID и исходного тела, разделяя первые три части переводом строки\n - 4
Вычислите HMAC-SHA-256 с 256-битным ключом, полученным при настройке webhook
- 5
Сравните ожидаемое значение
v1={hex}сX-Svoimail-Webhook-Signatureфункцией сравнения с постоянным временем - 6
Только после успешной проверки подписи разбирайте JSON, проверяйте поля события и выполняйте действие
Типы событий
Версия 1 использует только перечисленные значения. Неизвестный переход мониторинга безопасно отправляется как notification.created
Получатель должен игнорировать неизвестные типы будущих версий или направлять их в общий обработчик, а не завершать весь процесс с ошибкой
| event_type | Когда отправляется | Типичная severity |
|---|---|---|
notification.created
|
Создано обычное уведомление или уведомление без распознанного перехода мониторинга | info, success, warning или error
|
monitoring.incident.opened
|
Мониторинг подтвердил начало нового инцидента | warning или error
|
monitoring.incident.escalated
|
Продолжающийся инцидент повысился с warning до error | error
|
monitoring.incident.recovered
|
Проверка подтвердила восстановление после инцидента | success
|
Поля payload версии 1
| Поле | JSON-тип | Обязательное | Описание |
|---|---|---|---|
version
|
integer | Да | Версия контракта, сейчас всегда 1
|
event_id
|
string | Да | Стабильный в пределах одной ноды идентификатор вида notification-{id} для дедупликации
|
event_type
|
string enum | Да | Тип события из таблицы выше |
occurred_at
|
string date-time | Да | Время создания уведомления в UTC в формате RFC 3339 |
severity
|
string enum | Да | info, success, warning или error
|
category
|
string | Да | Категория уведомления, например monitoring
|
title
|
string | Да | Короткий заголовок для человека |
message
|
string | Да | Описание события для человека |
resource
|
object | Да | Связанный объект с полями type и id
|
resource.type
|
string | Да | Тип ресурса, например service, backup или backup_job
|
resource.id
|
string | Да | Идентификатор ресурса в строковом виде |
details
|
object | Да | Структурированные данные события, пустой объект если деталей нет |
Пример события мониторинга
{
"version": 1,
"event_id": "notification-1842",
"event_type": "monitoring.incident.opened",
"occurred_at": "2026-10-15T08:45:12.381Z",
"severity": "warning",
"category": "monitoring",
"title": "Очередь Postfix требует внимания",
"message": "В отложенной очереди Postfix находится 128 сообщений",
"resource": {
"type": "service",
"id": "postfix"
},
"details": {
"check_key": "postfix.queue.backlog",
"health_status": "firing",
"transition": "opened",
"incident_sequence": 3,
"incident_severity": "warning",
"failure_streak": 2,
"failure_threshold": 2,
"recovery_streak": 0,
"recovery_threshold": 2,
"first_failed_at": "2026-10-15T08:44:12.381Z",
"observed_at": "2026-10-15T08:45:12.381Z",
"details": {
"total_count": 136,
"deferred_count": 128,
"oldest_deferred_age_seconds": 16200
}
}
}
Поля details для мониторинга
| Поле | JSON-тип | Значения и назначение |
|---|---|---|
check_key
|
string | Стабильный ключ проверки, пригодный для маршрутизации |
health_status
|
string enum | firing для активной проблемы или healthy после восстановления
|
transition
|
string enum | opened, escalated или recovered
|
incident_sequence
|
integer | Порядковый номер инцидента одной проверки |
incident_severity
|
string enum | warning или error, исходный уровень инцидента
|
failure_streak
|
integer | Число последовательных неуспешных наблюдений |
failure_threshold
|
integer | Сколько неуспешных наблюдений требуется для открытия инцидента |
recovery_streak
|
integer | Число последовательных успешных наблюдений |
recovery_threshold
|
integer | Сколько успешных наблюдений требуется для восстановления |
first_failed_at
|
string date-time или null | Время первого неуспешного наблюдения текущего инцидента |
observed_at
|
string date-time | Время наблюдения, которое вызвало переход |
details
|
object | Данные конкретной проверки, набор полей зависит от check_key
|
Ключи встроенных проверок
Набор встроенных ключей может расширяться. Для маршрутизации используйте известные значения, а неизвестные отправляйте в общий канал мониторинга
| check_key | Что контролирует |
|---|---|
backup.last_result
|
Результат последней завершённой операции создания резервной копии |
backup.freshness
|
Наличие успешной резервной копии моложе 36 часов, критический порог 72 часа |
postfix.queue.snapshot
|
Доступность, актуальность и полноту агрегированного снимка очереди Postfix |
postfix.queue.backlog
|
Количество и возраст сообщений в deferred-очереди Postfix |
Пороги и данные встроенных проверок
| check_key | Порог открытия и восстановления | Поля details.details |
|---|---|---|
backup.last_result
|
Открытие после одной неуспешной backup-операции, восстановление после одной успешной | job_id integer, kind string enum, trigger string enum, stage string, finished_at string date-time или null
|
backup.freshness
|
Warning через 36 часов без успешной копии, error через 72 часа, восстановление после одной актуальной проверки. При выключенном расписании проверка healthy | enabled boolean, age_seconds integer, warning_age_seconds integer, critical_age_seconds integer, опциональные last_successful_job_id integer и last_successful_at string date-time или null
|
postfix.queue.snapshot
|
Открытие после трёх недоступных, устаревших или неполных снимков, восстановление после двух корректных. Снимок устаревает через 15 минут | available boolean, stale boolean, truncated boolean, invalid_records integer, sampled_at string date-time или null
|
postfix.queue.backlog
|
Warning при 100 deferred или при 10 deferred со старейшим от 4 часов. Error при 1000 deferred или при 10 deferred со старейшим от 24 часов. Открытие и восстановление после двух проверок | total_count integer, deferred_count integer, oldest_deferred_age_seconds integer или null
|
Как система обрабатывает HTTP-статусы
Заголовок Retry-After не изменяет расписание повторов: worker использует фиксированные интервалы из раздела ниже
| Ответ endpoint | Результат доставки |
|---|---|
Любой 2xx
|
Успешная доставка, повторов не будет |
408, 425, 429
|
Временная ошибка, доставка будет повторена |
Любой 5xx
|
Временная ошибка, доставка будет повторена |
Любой 3xx
|
Конечная ошибка, redirect не выполняется и повторов не будет |
Другой 4xx
|
Конечная ошибка запроса, повторов не будет |
Итоговый 1xx
|
Конечная ошибка протокола, повторов не будет |
| Ошибка DNS, TLS, соединения или таймаут | Временная ошибка, доставка будет повторена |
Внутренние статусы доставки
Статусы являются частью серверного механизма надёжности и не требуют ручного управления в панели
| Статус | Значение |
|---|---|
pending
|
Событие сохранено и ожидает первой попытки |
delivering
|
Worker получил временную аренду записи и выполняет запрос |
retry_wait
|
Предыдущая попытка завершилась временной ошибкой, назначено время следующей |
delivered
|
Endpoint вернул успешный статус 2xx
|
failed
|
Получена конечная ошибка или исчерпаны попытки |
cancelled
|
Администратор заменил или отключил endpoint до завершения доставки |
Расписание повторных попыток
После шестой временной ошибки доставка получает статус failed
Если worker остановился во время запроса и остались попытки, временная аренда истекает через минуту и запись снова становится доступной для обработки
| Попытка | Когда выполняется |
|---|---|
| 1 | Событие ставится в очередь сразу, worker проверяет очередь каждые 15 секунд |
| 2 | Через 1 минуту после первой ошибки |
| 3 | Через 5 минут после второй ошибки |
| 4 | Через 15 минут после третьей ошибки |
| 5 | Через 1 час после четвёртой ошибки |
| 6 | Через 6 часов после пятой ошибки |
Гарантия доставки и дедупликация
Событие и запись доставки создаются в одной транзакции базы данных, поэтому подтверждённое уведомление не теряется между сохранением и постановкой в очередь
Используется гарантия at-least-once. Если endpoint принял запрос, а worker остановился до сохранения результата, тот же запрос может прийти повторно
Для дедупликации сохраните event_id с уникальным ограничением и при повторном значении верните успешный 2xx без повторного выполнения действия
Если один обработчик принимает события нескольких нод, используйте отдельный route для каждой ноды и уникальность по паре route + event_id, потому что числовые ID разных нод могут совпасть
Порядок доставки обычно совпадает с порядком уведомлений, но не является контрактом при повторах и временных ошибках
Минимальные требования к обработчику
-
Принимать только
POSTсContent-Type: application/json -
Проверять timestamp и HMAC-подпись по исходным байтам тела до разбора JSON
-
Проверять
versionдо разбора полей конкретной версии -
Ограничить размер тела запроса на своей стороне
-
Сохранять
event_idи не выполнять одно действие дважды -
Возвращать
2xxпосле сохранения события, а не после долгой отправки во внешние сервисы -
Игнорировать неизвестные дополнительные поля
-
Записывать время,
event_id,event_typeи итог обработки без полного URL endpoint -
Отвечать без секретов, stack trace и внутренних данных
Безопасность endpoint
Полный URL и ключ подписи хранятся на ноде в зашифрованном виде с разным контекстом шифрования. В API и после сохранения в панели возвращаются только признак настройки и hostname
Версия 1 подписывает timestamp, event ID и точные байты тела алгоритмом HMAC-SHA-256. Проверяйте подпись до разбора JSON и используйте сравнение с постоянным временем
Не добавляйте логин и пароль через user:password@host: такой формат отклоняется. Не размещайте секреты в path или query string и исключите полный URL из журналов reverse proxy
Передавайте события только в доверенную систему, которая хранит payload и журналы с учётом содержащихся в них доменов, адресов ящиков, IP и других операционных данных
Ограничьте права получателя: webhook должен создавать уведомление или запись, но не должен получать административный доступ к почтовой ноде
Что происходит при замене и отключении
Новый URL вместе с новым ключом или отдельно заменённый ключ применяется только к уведомлениям, созданным после сохранения настройки
При замене URL или ключа незавершённые доставки со старой конфигурацией получают статус cancelled и не переносятся на новый endpoint
Запрос, уже отправленный в момент изменения настройки, может завершиться со старым URL или ключом. Получатель всё равно должен выполнять дедупликацию по event_id
После отключения новые уведомления продолжают появляться в панели, но записи webhook-доставки для них не создаются
Проверка при отсутствии событий
- 1
Убедитесь, что переключатель webhook включён и панель показывает ожидаемый hostname
- 2
Проверьте доступность HTTPS endpoint из интернета и срок действия его сертификата
- 3
Убедитесь, что endpoint не отвечает redirect и сразу обрабатывает исходный URL
- 4
Проверьте, что обработчик укладывается в 10 секунд
- 5
Проверьте журнал получателя по заголовку
X-Svoimail-Event-ID - 6
Для временной ошибки возвращайте
408,425,429или5xx, для принятого события возвращайте2xx - 7
После исправления создайте новое контролируемое уведомление и проверьте результат
Всё получилось по инструкции?
Выберите один вариант - комментарий писать не нужно