接收 Webhooks
概述
Remnawave 可以為許多事件發送 webhooks。
配置
.env 配置
WEBHOOK_ENABLED=true
WEBHOOK_URL=https://your-server.com/webhook
WEBHOOK_SECRET_HEADER=your-secret-header
| 變數 | 描述 |
|---|---|
WEBHOOK_ENABLED | 啟用 webhooks。 |
WEBHOOK_URL | 發送 webhook 的 URL。(必須以 https:// 或 http:// 開頭)。可以指定多個以逗號分隔的 URL(不帶空格) |
WEBHOOK_SECRET_HEADER | 此標頭將用於簽署 webhook 有效載荷。(只允許 aA-zZ、0-9) |
標頭
Remnawave 將隨 webhook 有效載荷發送以下標頭:
X-Remnawave-Signature- webhook 有效載荷的簽名。(使用 WEBHOOK_SECRET_HEADER 簽署)X-Remnawave-Timestamp- webhook 有效載荷的時間戳。
有效載荷
有效載荷將是一個 JSON 物件。
{
"scope": "service",
"event": "service.panel_started",
"timestamp": "2026-01-07T11:57:29.426Z",
"data": {
"panelVersion": "2.5.0"
}
}
屬性:
-
scope- webhook 有效載荷的範圍。(自 v2.5.0 起)user- 使用者事件user_hwid_devices- 使用者 HWID 裝置事件node- 節點事件service- 服務事件crm- 基礎設施計費事件torrent_blocker- 種子封鎖事件errors- 錯誤事件
-
event- 發生的事件。 -
timestamp- ISO 8601 格式的 webhook 有效載荷時間戳。 -
data- 與事件相關的資料。
每個範圍的詳細有效載荷模式可在 OpenAPI 文件中找到。
👉 參考下面每個範圍部分的 Model Link。
範圍:user
OpenAPI 模型:RemnawaveWebhookUserEventsDto
模型連結: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- 使用者未連接(僅當.env中NOT_CONNECTED_USERS_NOTIFICATIONS_ENABLED為 true 時激活)user.expiration- 用戶到期通知(僅當.env中EXPIRATION_NOTIFICATIONS_ENABLED為 true 時啟用。)
Remnawave Typescript SDK 類型:
import { TRemnawaveWebhookUserEvent, RemnawaveWebhookUserEvents } from '@remnawave/backend-contract'
meta 物件
大多數用戶事件的 meta 為 null。僅通知類事件會填充此欄位,提供觸發 webhook 的上下文。
| 欄位 | 類型 | 適用事件 | 說明 |
|---|---|---|---|
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 模型:RemnawaveWebhookUserHwidDevicesEventsDto
模型連結: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 模型:RemnawaveWebhookNodeEventsDto
模型連結: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 模型:RemnawaveWebhookServiceEventsDto
模型連結: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 模型:RemnawaveWebhookCrmEventsDto
模型連結: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- 頻寬使用閾值已達到最大通知次數
驗證 Webhook
Remnawave 將使用 WEBHOOK_SECRET_HEADER 簽署 webhook 有效載荷並發送到 WEBHOOK_URL。
您可以通過檢查簽名來驗證 webhook 有效載荷。
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)
}