Получение вебхуков
Обзор
Remnawave может отправлять вебхуки для многих событий.
Конфигурация
Конфигурация .env
WEBHOOK_ENABLED=true
WEBHOOK_URL=https://your-server.com/webhook
WEBHOOK_SECRET_HEADER=your-secret-header
| Переменная | Описание |
|---|---|
WEBHOOK_ENABLED | Включить вебхуки. |
WEBHOOK_URL | URL для отправки вебхука. (должен начинаться с https:// или http://). Можно указать несколько URL, разделённых запятыми (без пробелов) |
WEBHOOK_SECRET_HEADER | Этот заголовок будет использоваться для подписи полезной нагрузки вебхука. (допускаются только aA-zZ, 0-9) |
Заголовки
Remnawave будет отправлять следующие заголовки с полезной нагрузкой вебхука:
X-Remnawave-Signature- Подпись полезной нагрузки вебхука. (подписывается с помощью WEBHOOK_SECRET_HEADER)X-Remnawave-Timestamp- Метка времени полезной нагрузки вебхука.
Полезная нагрузка
Полезная нагрузка будет JSON-объектом.
{
"scope": "service",
"event": "service.panel_started",
"timestamp": "2026-01-07T11:57:29.426Z",
"data": {
"panelVersion": "2.5.0"
}
}
Свойства:
-
scope- Область вебхука. (Начиная с v2.5.0)user- События пользователяuser_hwid_devices- События устройств HWID пользователяnode- События узлаservice- События сервисаcrm- События инфра-биллингаtorrent_blocker- События блокировщика торрентовerrors- События ошибок
-
event- Произошедшее событие. -
timestamp- Метка времени в формате ISO 8601. -
data- Данные, связанные с событием.
Подробная схема полезной нагрузки для каждой области доступна в документации OpenAPI.
👉 Обратитесь к Model Link в каждом разделе области ниже.
Область: user
OpenAPI Model: RemnawaveWebhookUserEventsDto
Model Link: https://rw.gy/api/#model/RemnawaveWebhookUserEventsDto
Доступные события (свойство event):
user.created- Пользователь созданuser.modified- Пользователь изменёнuser.deleted- Пользователь удалёнuser.revoked- Пользователь отозванuser.disabled- Пользователь отключёнuser.enabled- Пользователь включёнuser.limited- Пользователь ограниченuser.expired- Срок действия пользователя истёкuser.traffic_reset- Трафик пользователя сброшенuser.expires_in_72_hours- Срок действия пользователя истекает через 72 часа (удалено в v.2.8.0, используйтеuser.expiration)user.expires_in_48_hours- Срок действия пользователя истекает через 48 часов (удалено в v.2.8.0, используйтеuser.expiration)user.expires_in_24_hours- Срок действия пользователя истекает через 24 часа (удалено в v.2.8.0, используйтеuser.expiration)user.expired_24_hours_ago- Срок действия пользователя истёк 24 часа назад (удалено в v.2.8.0, используйтеuser.expiration)user.first_connected- Пользователь впервые подключилсяuser.bandwidth_usage_threshold_reached- Достигнут порог использования полосы пропусканияuser.not_connected- Пользователь не подключён (активно только когдаNOT_CONNECTED_USERS_NOTIFICATIONS_ENABLEDравно true в.env)user.expiration- Уведомления об истечении срока действия пользователя (активно только когдаEXPIRATION_NOTIFICATIONS_ENABLEDравно true в.env.)
Типы Remnawave Typescript SDK:
import { TRemnawaveWebhookUserEvent, RemnawaveWebhookUserEvents } from '@remnawave/backend-contract'
Объект meta
Большинство пользовательских событий содержат meta: null. Поле заполняется только для событий-уведомлений, где оно предоставляет контекст, вызвавший вебхук.
| Поле | Тип | Присутствует для | Описание |
|---|---|---|---|
notConnectedAfterHours | number | null | user.not_connected | Часов офлайн. Соответствует порогу из NOT_CONNECTED_USERS_NOTIFICATIONS_AFTER_HOURS, вызвавшему событие. |
expiration | number | null | user.expiration | Знаковое смещение в часах относительно истечения срока действия (expireAt). Соответствует значению из EXPIRATION_NOTIFICATIONS. |
Знак поля expiration
Значение expiration знаковое и кодирует направление относительно момента истечения:
- Отрицательное — сработало до истечения: истекает через
|N|часов (например,-72→ истекает через 72 часа). - Положительное — сработало после истечения: истёк
Nчасов назад (например,24→ истёк 24 часа назад).
Для всех событий, кроме user.not_connected и user.expiration, meta равно null. При наличии meta заполнено только поле для текущего события — другое поле остаётся null.
Область: user_hwid_devices
OpenAPI Model: RemnawaveWebhookUserHwidDevicesEventsDto
Model Link: https://rw.gy/api/#model/RemnawaveWebhookUserHwidDevicesEventsDto
Доступные события:
user_hwid_devices.added- Устройство HWID пользователя добавленоuser_hwid_devices.deleted- Устройство HWID пользователя удалено
import {
TRemnawaveWebhookUserHwidDevicesEvent,
RemnawaveWebhookUserHwidDevicesEvents
} from '@remnawave/backend-contract'
Область: node
OpenAPI Model: RemnawaveWebhookNodeEventsDto
Model Link: https://rw.gy/api/#model/RemnawaveWebhookNodeEventsDto
Доступные события:
node.created- Узел созданnode.modified- Узел изменёнnode.disabled- Узел отключёнnode.enabled- Узел включёнnode.deleted- Узел удалёнnode.connection_lost- Соединение с узлом потеряноnode.connection_restored- Соединение с узлом восстановленоnode.traffic_notify- Уведомление о трафике узла
import { TRemnawaveWebhookNodeEvent, RemnawaveWebhookNodeEvents } from '@remnawave/backend-contract'
Область: service
OpenAPI Model: RemnawaveWebhookServiceEventsDto
Model Link: https://rw.gy/api/#model/RemnawaveWebhookServiceEventsDto
Доступные события:
service.panel_started- Панель запущенаservice.login_attempt_failed- Попытка входа не удаласьservice.login_attempt_success- Попытка входа успешнаservice.subpage_config_changed- Конфигурация подстраницы изменена
import {
TRemnawaveWebhookServiceEvent,
RemnawaveWebhookServiceEvents
} from '@remnawave/backend-contract'
Область: crm
OpenAPI Model: RemnawaveWebhookCrmEventsDto
Model Link: https://rw.gy/api/#model/RemnawaveWebhookCrmEventsDto
Доступные события:
crm.infra_billing_node_payment_in_7_days- Оплата узла инфра-биллинга через 7 днейcrm.infra_billing_node_payment_in_48hrs- Оплата узла инфра-биллинга через 48 часовcrm.infra_billing_node_payment_in_24hrs- Оплата узла инфра-биллинга через 24 часаcrm.infra_billing_node_payment_due_today- Оплата узла инфра-биллинга сегодняcrm.infra_billing_node_payment_overdue_24hrs- Просрочка оплаты узла 24 часаcrm.infra_billing_node_payment_overdue_48hrs- Просрочка оплаты узла 48 часовcrm.infra_billing_node_payment_overdue_7_days- Просрочка оплаты узла 7 дней
import { TRemnawaveWebhookCrmEvent, RemnawaveWebhookCrmEvents } from '@remnawave/backend-contract'
Область: torrent_blocker
Начиная с Remnawave Panel v2.7.0.
OpenAPI Model: RemnawaveWebhookTorrentBlockerEventsDto
Model Link: https://rw.gy/api/#model/RemnawaveWebhookTorrentBlockerEventsDto
Доступные события:
torrent_blocker.report- Отчёт блокировщика торрентов
import {
TRemnawaveWebhookTorrentBlockerEvent,
RemnawaveWebhookTorrentBlockerEvents
} from '@remnawave/backend-contract'
Область: errors
OpenAPI Model: RemnawaveWebhookErrorsEventsDto
Model Link: https://rw.gy/api/#model/RemnawaveWebhookErrorsEventsDto
Доступные события:
errors.bandwidth_usage_threshold_reached_max_notifications- Достигнуто максимальное число уведомлений о пороге полосы пропускания
Проверка вебхука
Remnawave подпишет полезную нагрузку вебхука с помощью WEBHOOK_SECRET_HEADER и отправит её на WEBHOOK_URL.
Вы можете проверить полезную нагрузку вебхука, проверив подпись.
export interface WebhookHeaders {
'x-remnawave-signature': string
'x-remnawave-timestamp': string
}
validateWebhook(data: {
body: unknown
headers: WebhookHeaders
}): boolean {
if (!this.webhookSecret) return false
const signature = createHmac('sha256', this.webhookSecret)
.update(JSON.stringify(data.body))
.digest('hex')
return signature === data.headers['x-remnawave-signature']
}
Примеры для разных языков
Python
Пример кода Python
def validate_webhook(body, signature):
webhook_secret_panel = "your_secret_token"
"""Validate webhook signature"""
if isinstance(body, str):
original_body = body
logging.warning("Body is string, parsing for logging...")
try:
parsed_body = json.loads(body)
except json.JSONDecodeError as e:
logging.warning("Failed to parse body: %s", e)
return False
else:
original_body = json.dumps(body, separators=(',', ':'))
parsed_body = body
computed_signature = hmac.new(
webhook_secret_panel.encode('utf-8'),
original_body.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed_signature, signature)
Go
Пример кода Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
)
var webhookSecret = "your-secret-header"
type WebhookPayload struct {
Event string `json:"event"`
Data json.RawMessage `json:"data"`
Timestamp string `json:"timestamp"`
}
func validateWebhook(body []byte, signature string) bool {
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write(body)
expectedMAC := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expectedMAC))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusBadRequest)
return
}
signature := r.Header.Get("X-Remnawave-Signature")
if !validateWebhook(body, signature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Webhook received"))
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
fmt.Println("Server running at http://localhost:3000")
http.ListenAndServe(":3000", nil)
}