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

Вебхуки

Вебхуки позволяют панели самой сообщать внешним системам о том, что произошло: создан пользователь, упала нода, закончился трафик. Панель отправляет запрос POST с JSON-телом на ваш адрес, поэтому принимать его может скрипт, CRM, система мониторинга или чат-бот.

Включение​

Вебхуки выключены по умолчанию. Настройки задаются в .env панели:

ПеременнаяПо умолчаниюОписание
WEBHOOK_ENABLEDfalseвключает отправку вебхуков
WEBHOOK_URL—адрес получателя; несколько адресов — через запятую
WEBHOOK_SECRET_HEADER—секрет подписи payload, ровно 64 символа [a-zA-Z0-9]
WEBHOOK_ENABLED=true
WEBHOOK_URL=https://example.com/xlada/webhook
WEBHOOK_SECRET_HEADER=vsmu67Kmg6R8FjIOF1WUY8LWBHie4scdEqrfsKmyf4IAf8dY3nFS0wwYHkhh6ZvQ
Внимание

При WEBHOOK_ENABLED=true обе переменные обязательны. WEBHOOK_URL должен начинаться с http:// или https://, иначе контейнер не стартует. Секрет подписи храните только в .env — по нему получатель проверяет подлинность запросов.

Какие события отправлять, решает файл /var/lib/remnawave/configs/notifications/notifications-config.yml внутри контейнера. По умолчанию в образе включены все события по обоим каналам. Событие, которого нет в файле, считается выключенным. Чтобы правки сохранялись, смонтируйте свой файл поверх каталога /var/lib/remnawave/configs/notifications.

events:
user.created:
telegram: true
webhook: true
user.expired:
telegram: false
webhook: true
# дополнительный адрес только для этого события
additionalWebhookUrls:
- https://example.com/xlada/critical

Подпись payload​

С каждым запросом панель отправляет служебные заголовки:

ЗаголовокЗначение
X-XLADA-SignatureHMAC-SHA256 от тела запроса, в hex
X-XLADA-Timestampвремя события в ISO 8601
Content-Typeapplication/json
User-AgentXLADA

Подпись считается как HMAC-SHA256(секрет, тело_запроса) и сравнивается с заголовком. Сравнивать нужно именно исходное тело, а не разобранный JSON.

import { createHmac, timingSafeEqual } from 'node:crypto';

function isValidSignature(rawBody, secret, signature) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}

Формат payload​

В теле всегда есть четыре поля: scope, event, timestamp и data. У событий пользователя дополнительно есть meta.

{
"scope": "service",
"event": "service.panel_started",
"timestamp": "2026-01-07T11:57:29.426Z",
"data": {
"panelVersion": "1.1.1"
}
}

Поле meta заполняется только у событий-напоминаний:

ПолеСобытиеЗначение
notConnectedAfterHoursuser.not_connectedсколько часов пользователь не подключался
expirationuser.expirationчасы относительно expireAt: минус — до истечения, плюс — после

У всех остальных событий meta равно null.

Список событий​

Область (scope)События
useruser.created, user.modified, user.deleted, user.revoked, user.disabled, user.enabled, user.limited, user.expired, user.traffic_reset, user.first_connected, user.bandwidth_usage_threshold_reached, user.not_connected, user.expiration
user_hwid_devicesuser_hwid_devices.added, user_hwid_devices.deleted
nodenode.created, node.modified, node.disabled, node.enabled, node.deleted, node.connection_lost, node.connection_restored, node.traffic_notify
serviceservice.panel_started, service.login_attempt_failed, service.login_attempt_success, service.subpage_config_changed, service.api_token_created, service.api_token_deleted
errorserrors.bandwidth_usage_threshold_reached_max_notifications
crmcrm.infra_billing_node_payment_in_7_days, crm.infra_billing_node_payment_in_48hrs, crm.infra_billing_node_payment_in_24hrs, crm.infra_billing_node_payment_due_today, crm.infra_billing_node_payment_overdue_24hrs, crm.infra_billing_node_payment_overdue_48hrs, crm.infra_billing_node_payment_overdue_7_days
torrent_blockertorrent_blocker.report
Примечание

События user.not_connected и user.expiration приходят только при включённых NOT_CONNECTED_USERS_NOTIFICATIONS_ENABLED и EXPIRATION_NOTIFICATIONS_ENABLED. События user_hwid_devices.* появляются, только когда включён лимит устройств, и уходят исключительно в вебхуки — в Telegram они не дублируются.

Повторные попытки​

Доставка идёт через очередь: на каждый адрес создаётся отдельная задача с ограничением в 100 одновременных отправок. Если получатель не ответил, запрос повторяется 3 раза с паузой 5 секунд — всего до четырёх попыток. После этого ошибка пишется в лог панели, а задача остаётся в очереди с ограниченным хранением.

В notifications-config.yml можно задать дополнительные адреса для конкретного события (additionalWebhookUrls). Они добавляются к адресам из WEBHOOK_URL, а не заменяют их.

Совет

Отдавайте на вебхук 2xx сразу, а обработку выносите в фон. Пока получатель не ответит, панель держит соединение, а медленные ответы копят очередь.

Что дальше​