Обслуживание

Webhook уведомлений почтовой ноды

Полный контракт webhook: настройка endpoint, формат событий, статусы доставки, повторные попытки, безопасность и обработка ошибок

18 минут Автор: «Своя Почта»
Все разделы документации
Содержание статьи

Что отправляет webhook

Webhook передаёт во внешнюю систему каждое новое уведомление, которое появляется в панели ноды после включения интеграции

Он подходит для собственного обработчика, системы мониторинга, автоматизации или промежуточного сервиса, который отправляет сообщения в нужный мессенджер

Webhook не передаёт письма, вложения, пароли почтовых аккаунтов и содержимое почтовых ящиков

Текст уведомления и details могут содержать домен, адрес почтового ящика, IP сервера или hostname источника импорта. Считайте payload конфиденциальными операционными данными

Как подключить endpoint

После сохранения панель показывает только hostname endpoint. Полный URL повторно не возвращается и остаётся пустым в поле редактирования

Ключ подписи после сохранения тоже не возвращается. Чтобы оставить текущий endpoint и ключ без изменений, сохраните форму с пустыми полями. При замене URL обязательно создайте новый отличающийся ключ. Чтобы заменить только ключ, заполните его поле. Чтобы отключить доставку, выключите переключатель

  1. 1

    Войдите в панель с ролью владельца

  2. 2

    Откройте «Сервер» → «Настройки системы»

  3. 3

    Включите «Webhook уведомлений»

  4. 4

    Укажите полный HTTPS URL обработчика

  5. 5

    Сгенерируйте ключ подписи и скопируйте его в конфигурацию обработчика до сохранения формы

  6. 6

    Сохраните настройки

  7. 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. 1

    Получите исходные байты тела без предварительного разбора JSON и преобразования кодировки

  2. 2

    Проверьте, что X-Svoimail-Webhook-Timestamp содержит целое Unix-время и отличается от текущего времени не более чем на 5 минут

  3. 3

    Составьте подписываемое значение из v1, timestamp, event ID и исходного тела, разделяя первые три части переводом строки \n

  4. 4

    Вычислите HMAC-SHA-256 с 256-битным ключом, полученным при настройке webhook

  5. 5

    Сравните ожидаемое значение v1={hex} с X-Svoimail-Webhook-Signature функцией сравнения с постоянным временем

  6. 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 Да Структурированные данные события, пустой объект если деталей нет

Пример события мониторинга

JSON payload
{
  "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. 1

    Убедитесь, что переключатель webhook включён и панель показывает ожидаемый hostname

  2. 2

    Проверьте доступность HTTPS endpoint из интернета и срок действия его сертификата

  3. 3

    Убедитесь, что endpoint не отвечает redirect и сразу обрабатывает исходный URL

  4. 4

    Проверьте, что обработчик укладывается в 10 секунд

  5. 5

    Проверьте журнал получателя по заголовку X-Svoimail-Event-ID

  6. 6

    Для временной ошибки возвращайте 408, 425, 429 или 5xx, для принятого события возвращайте 2xx

  7. 7

    После исправления создайте новое контролируемое уведомление и проверьте результат

Всё получилось по инструкции?

Выберите один вариант - комментарий писать не нужно