본문으로 건너뛰기

Webhook 수신

개요

Remnawave는 많은 이벤트에 대해 webhook을 보낼 수 있습니다.

구성

.env 구성

WEBHOOK_ENABLED=true
WEBHOOK_URL=https://your-server.com/webhook
WEBHOOK_SECRET_HEADER=your-secret-header
변수설명
WEBHOOK_ENABLEDwebhook 활성화.
WEBHOOK_URLwebhook을 보낼 URL. (https:// 또는 http://로 시작해야 합니다). 쉼표로 구분하여 여러 URL 지정 가능 (공백 없이)
WEBHOOK_SECRET_HEADERwebhook 페이로드 서명에 사용될 헤더. (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 - 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을 트리거한 컨텍스트를 제공합니다.

필드타입해당 이벤트설명
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_connecteduser.expiration 외의 모든 이벤트에서 metanull입니다. 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로 보냅니다.

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