Webhook 수신
개요
Remnawave는 많은 이벤트에 대해 webhook을 보낼 수 있습니다.
구성
.env 구성
WEBHOOK_ENABLED=true
WEBHOOK_URL=https://your-server.com/webhook
WEBHOOK_SECRET_HEADER=your-secret-header
| 변수 | 설명 |
|---|---|
WEBHOOK_ENABLED | webhook 활성화. |
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 형식의 타임스탬프. -
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- 사용자 미연결 (.env에서NOT_CONNECTED_USERS_NOTIFICATIONS_ENABLED가 true일 때만 활성)user.expiration- 사용자 만료 알림 (.env에서EXPIRATION_NOTIFICATIONS_ENABLED가 true일 때만 활성화됩니다.)
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
사용 가능한 이벤트:
user_hwid_devices.added- 사용자 HWID 장치 추가됨user_hwid_devices.deleted- 사용자 HWID 장치 삭제됨
범위: node
사용 가능한 이벤트:
node.created- 노드 생성됨node.modified- 노드 수정됨node.disabled- 노드 비활성화됨node.enabled- 노드 활성화됨node.deleted- 노드 삭제됨node.connection_lost- 노드 연결 끊김node.connection_restored- 노드 연결 복구됨node.traffic_notify- 노드 트래픽 알림
범위: service
사용 가능한 이벤트:
service.panel_started- 패널 시작됨service.login_attempt_failed- 로그인 시도 실패service.login_attempt_success- 로그인 시도 성공service.subpage_config_changed- 서브페이지 구성 변경됨
범위: crm
사용 가능한 이벤트:
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일 초과
범위: torrent_blocker
Remnawave Panel v2.7.0 이상.
torrent_blocker.report- 토렌트 차단 보고서
범위: errors
errors.bandwidth_usage_threshold_reached_max_notifications- 대역폭 임계값 최대 알림 도달
Webhook 검증
Remnawave는 WEBHOOK_SECRET_HEADER로 webhook 페이로드에 서명하고 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"
if isinstance(body, str):
original_body = body
try:
parsed_body = json.loads(body)
except json.JSONDecodeError as 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"
"fmt"
"io/ioutil"
"net/http"
)
var webhookSecret = "your-secret-header"
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, _ := ioutil.ReadAll(r.Body)
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)
}