إنتقل إلى المحتوى الرئيسي

استقبال 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عنوان URL لإرسال webhook إليه. (يجب أن يبدأ بـ 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.

مثال على حمولة webhook
{
"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 - الطابع الزمني لحمولة webhook بتنسيق ISO 8601.

  • 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 - مستخدم غير متصل (نشط فقط عندما تكون NOT_CONNECTED_USERS_NOTIFICATIONS_ENABLED صحيحة في .env)
  • user.expiration - إشعارات انتهاء صلاحية المستخدم (نشط فقط عندما يكون EXPIRATION_NOTIFICATIONS_ENABLED صحيحاً في .env.)

أنواع Remnawave Typescript SDK:

import { TRemnawaveWebhookUserEvent, RemnawaveWebhookUserEvents } from '@remnawave/backend-contract'

كائن meta

تحمل معظم أحداث المستخدم meta: null. يُملأ هذا الحقل فقط لأحداث الإشعارات، حيث يوفر السياق الذي أطلق الـ webhook.

الحقلالنوعموجود لـالوصف
notConnectedAfterHoursnumber | nulluser.not_connectedساعات اتصال المستخدم. يطابق الحد من NOT_CONNECTED_USERS_NOTIFICATIONS_AFTER_HOURS الذي أطلق الحدث.
expirationnumber | nulluser.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 بـ WEBHOOK_SECRET_HEADER ويرسلها إلى WEBHOOK_URL.

يمكنك التحقق من حمولة webhook عن طريق التحقق من التوقيع.

التحقق من 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"`
}

type UserData struct {
UUID string `json:"uuid"`
Username string `json:"username"`
Status string `json:"status"`
UsedTrafficBytes string `json:"usedTrafficBytes"`
// Add other fields as needed
}

type NodeData struct {
UUID string `json:"uuid"`
Name string `json:"name"`
IsConnected bool `json:"isConnected"`
// Add other fields as needed
}

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")
timestamp := r.Header.Get("X-Remnawave-Timestamp")

if !validateWebhook(body, signature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}

var payload WebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Error parsing JSON", http.StatusBadRequest)
return
}

if strings.HasPrefix(payload.Event, "user.") {
var userData UserData
if err := json.Unmarshal(payload.Data, &userData); err != nil {
http.Error(w, "Error parsing user data", http.StatusBadRequest)
return
}

fmt.Printf("User event %s for %s\n", payload.Event, userData.Username)

switch payload.Event {
case "user.created":
// Handle user created
case "user.expired":
// Handle user expired
}
} else if strings.HasPrefix(payload.Event, "node.") {
var nodeData NodeData
if err := json.Unmarshal(payload.Data, &nodeData); err != nil {
http.Error(w, "Error parsing node data", http.StatusBadRequest)
return
}

fmt.Printf("Node event %s for %s\n", payload.Event, nodeData.Name)

switch payload.Event {
case "node.connection_lost":
// Handle node connection lost
case "node.connection_restored":
// Handle node connection restored
}
}

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)
}