위키 / Core Features / 아웃고잉 웹훅

아웃고잉 웹훅

업데이트한 사람 Maxime_48 · 3시간 전 · 조회수: 70

언어: English Français Deutsch Español Português (Brasil) Italiano Nederlands Polski Türkçe Русский 日本語 한국어 中文

웹훅은 팀의 활동이 일어나는 순간 내가 가진 URL로 보내 줘요. 감사 로그에 남는 모든 행동을 서명된 JSON POST로 내 서버, 내 봇, 사내 도구에 전달할 수 있어서 폴링도, 내보내기도, 패널 스크래핑도 필요 없어요.

이 페이지의 모든 것을 결정하는 규칙 하나. 웹훅은 팀의 활동 로그에 보이는 내용을 그대로 전달하고, 그 이상은 전달하지 않아요. 별도의 범위를 가진 병렬 피드도, 기계용으로 더 풍부한 페이로드도 아닙니다. 그 페이지에 없으면 본문에도 없어요.

전달 카테고리와 마지막 전송 시각이 표시된 활성 상태의 팀 웹훅


📍 어디에 있나요

팀 자체 페이지의 팀 설정 → 웹훅에 있어요. 팀당 엔드포인트는 최대 세 개이며, 각각 자신의 URL, 카테고리, 서명 비밀 키를 가집니다.

누가 설정할 수 있나요: 팀 소유자, 또는 역할에 팀에서 서버 제거 권한이 포함된 멤버예요. 활동 로그 자체를 여는 규칙과 같아요. 웹훅은 그 페이지가 보여 주는 내용을 전달하므로, 읽을 수 있다는 것은 전달할 수 있다는 뜻이에요. 팀 역할과 권한을 참고하세요.


🎯 전송되는 것과 절대 전송되지 않는 것

카테고리는 직접 고르세요. 체크한 카테고리만 전달돼요.

카테고리 포함되는 내용
기능 기능을 활성화하거나 비활성화함, 또는 기능 설정을 저장함
서버 서버를 팀에 추가하거나 팀에서 제거함
팀 팀 이름 변경, 역할 편집, 웹훅 변경
구성원 멤버를 초대함, 추가함, 제거함, 또는 멤버가 나감
모더레이션 제재, 이의 신청, 신고, 티켓, 삭제된 기록
구독 팀의 구독 변경
결제 팀의 결제 이벤트
이 팀에 대한 플랫폼 조치 플랫폼 관리자가 내 서버 중 하나에 한 일

🚫 절대 전달되지 않는 세 가지

  • IP 주소. 활동 로그에서 IP는 해당 본인에게만 보이고 다른 누구에게도 보이지 않아요. 웹훅에는 확인할 독자가 없으므로 이 필드는 전송되지 않아요. 비워 두는 것이 아니라 페이로드에서 제거됩니다.
  • 나와 관련 없는 백오피스 작업. 플랫폼 전체 관리에는 팀이 없으므로, 전달 과정의 어떤 것도 이에 닿을 수 없어요. 이 팀에 대한 플랫폼 조치 카테고리는 관리자가 내 서버에 한 일만 다룹니다.
  • 인증 정보. 토큰, 키, 비밀 값처럼 보이는 필드는 변경 전/후 쌍의 양쪽 모두 ••••••••로 도착해요. 이 처리는 페이지가 하는 마스킹과는 별개로, 내보내는 단계에서 한 번 더 이루어져요.

로그인은 제공되지 않아요. 감사 기록에는 auth 카테고리가 있지만, 위 목록에는 일부러 빠져 있어요. 로그인 이벤트를 제3자 서버에 무인으로 끝없이 보내는 것은, 누군가 직접 열어야 하는 페이지에 보여 주는 것과는 다른 행위이기 때문이에요. 나중에 추가될 수도 있어요.


📦 전달 내용

각 전달은 JSON 본문이 담긴 POST 한 번이에요.

{
  "version": 1,
  "delivery_id": "0a4b2f0e-8c6e-4a5f-9b3d-2f6c1e0d7a11",
  "event": "server.feature.enabled",
  "category": "feature",
  "team": { "id": 12, "name": "Night Shift" },
  "data": {
    "id": 918273,
    "action": "server.feature.enabled",
    "category": "feature",
    "description": "Enabled Starboard",
    "actor_name": "Ava",
    "actor_role": "moderator",
    "is_admin_action": false,
    "team_id": 12,
    "team_name": "Night Shift",
    "server_id": 4,
    "server_name": "Night Shift HQ",
    "subject_label": "Starboard",
    "changes": { "threshold": { "before": 3, "after": 5 } },
    "metadata": null,
    "created_at": "2026-09-06T09:41:12+00:00"
  }
}

이 열다섯 개 필드가 data의 전부예요. 활동 로그가 보여 주는 열다섯 개와 같고, IP 주소만 빠져 있어요. 어느 필드든 null일 수 있어요. 서버가 없는 행동은 "server_id": null을, 비교할 것이 없는 행동은 "changes": null을 가집니다. 이벤트마다 형태가 정해져 있다고 가정하지 말고 방어적으로 읽으세요.

  • **version**은 봉투(envelope)의 버전이에요. 형태가 늘어날 때 수신 측이 추측하지 않고 이 값으로 분기할 수 있도록 있는 필드예요.
  • **event**는 감사 대상이 된 행동이에요. 130개가 넘고 플랫폼과 함께 계속 늘어나므로, 이름을 빠짐없이 나열하지 말고 접두사나 category로 매칭하세요.
  • **delivery_id**는 같은 이벤트를 재시도해도 변하지 않아요. 중복 제거를 가능하게 하는 값이에요. 아래를 참고하세요.
  • **data**는 감사 항목 자체로, 활동 로그가 보여 주는 것과 같은 객체예요.

📨 헤더

헤더 값
User-Agent YAWBDB-Webhooks/1.0
X-YAWBDB-Event 행동 이름이며, 본문의 event와 같아요
X-YAWBDB-Delivery 전달 ID이며, delivery_id와 같아요
X-YAWBDB-Timestamp 요청을 만든 시각이며, 초 단위 Unix 시간이에요
X-YAWBDB-Signature v1= 뒤에 16진수 다이제스트가 붙어요

🔐 서명 검증하기

엔드포인트 URL을 아는 사람은 누구나 그곳에 무엇이든 POST할 수 있어요. 서명은 우리가 보낸 것과 그들이 보낸 것을 구별하는 방법이에요.

다이제스트는 타임스탬프, 점, 원본 요청 본문을 이어 붙인 값에 대한 HMAC-SHA256이며, 서명 비밀 키를 키로 사용해요.

signed_string = X-YAWBDB-Timestamp + "." + raw_body
signature     = "v1=" + hex( hmac_sha256(signed_string, secret) )

세 가지 세부 사항이 핵심이고, 하나라도 빠뜨리면 허점이 생겨요.

  • 원본 바이트에 서명하세요. JSON을 파싱하기 전의 값이에요. 본문을 다시 직렬화하면 키 순서, 이스케이프된 슬래시, 유니코드가 달라져서 다이제스트가 맞지 않아요.
  • 타임스탬프는 서명된 문자열 안에 들어 있어요. 단지 옆에 붙어 있는 것이 아니에요. 그래서 재전송 공격을 거부할 수 있어요. 타임스탬프가 몇 분 넘게 오래된 전달은 거부하세요(5분이 적당한 허용 범위예요). 그러면 공격자가 지난주에 가로챈 요청을 다시 보낼 수 없어요. 타임스탬프도 서명되어 있으니 수정할 수도 없고요.
  • 상수 시간으로 비교하세요. 일반적인 ==는 처음으로 다른 바이트에서 바로 반환하는데, 그 시간 차이만으로 서명을 한 바이트씩 알아낼 수 있어요. hash_equals, crypto.timingSafeEqual, hmac.compare_digest 등 사용하는 언어에서 부르는 이름을 쓰세요.

PHP

$raw       = file_get_contents('php://input');
$timestamp = (int) ($_SERVER['HTTP_X_YAWBDB_TIMESTAMP'] ?? 0);
$given     = $_SERVER['HTTP_X_YAWBDB_SIGNATURE'] ?? '';

if (abs(time() - $timestamp) > 300) {
    http_response_code(400);   // too old, or clock is wrong
    exit;
}

$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);

if (!hash_equals($expected, $given)) {
    http_response_code(401);
    exit;
}

http_response_code(200);       // accepted

Node.js (Express)

import crypto from 'node:crypto';

// The raw body is required - express.json() alone destroys it.
app.post('/yawbdb', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = Number(req.get('X-YAWBDB-Timestamp'));
  const given     = req.get('X-YAWBDB-Signature') ?? '';

  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(400);

  const expected = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${req.body}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(given);

  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  res.sendStatus(200);
});

v1= 접두사는 방식의 이름이에요. 나중에 바뀌더라도, 접두사를 확인하는 수신 측은 알 수 없는 방식을 조용히 잘못 읽는 대신 의도적으로 거부할 수 있어요.

🔑 비밀 키

16진수 64자로, 자동으로 생성되고 단 한 번만 표시돼요. 엔드포인트를 만든 직후, 또는 새 비밀 키를 누른 직후 패널에서 볼 수 있어요. 그 뒤에는 어디서도 다시 읽을 수 없어요. 페이지도, API도, 내보내기도 마찬가지예요. 잃어버리면 새로 생성해야 합니다.

키를 교체하면 그 순간부터 보내는 모든 전달에 즉시 적용돼요. 이미 대기열에 들어간 전달은 대기열이 빌 때까지 이전 비밀 키로 서명되어 계속 나가므로, 잠깐 겹치는 시간이 있을 수 있어요. 수신 측을 먼저 업데이트하세요.


🔁 전달, 재시도, 중복 제거

  • 2xx만 수신된 것으로 간주돼요. 그 외에는 3xx, 4xx, 5xx 모두 실패예요. 빨리 응답하고 작업은 그 뒤에 하세요.
  • 리다이렉트는 따라가지 않아요. 302는 한 번 건너가는 것이 아니라 실패예요. 최종 URL을 알려 주세요.
  • 타임아웃은 짧아요. 연결에 3초, 응답에 5초예요. 이것은 원격 프로시저 호출이 아니라 알림이에요. 먼저 확인 응답을 하고 처리는 나중에 하세요.
  • 네 번 시도해요. 간격은 30초, 2분, 10분이고 처음부터 끝까지 약 12분이에요. 배포 중 잠깐 끊기는 상황은 커버하지만 장애는 커버하지 못해요.
  • 재시도는 같은 delivery_id를 재사용해요. 모든 POST를 새 것으로 취급하는 수신 측은, 우리의 첫 시도가 내 쪽에서는 성공했지만 우리 쪽에서는 타임아웃이 난 날 이중으로 집계하게 됩니다. ID를 저장해 두고 이미 처리한 ID는 무시하세요.
  • 순서는 보장되지 않아요. 전달은 각각 독립적으로 대기열에 들어가므로, 재시도된 전달은 그 뒤에 일어난 이벤트보다 늦게 도착할 수 있어요. 순서가 중요하다면 data.created_at을 사용하세요.

⛔ 자동 중지

연속 20회 실패하면 엔드포인트가 중지되고, 패널의 카드에 그렇게 표시돼요. 응답하지 않게 된 엔드포인트는 대개 영영 멈춘 것이고 아무도 우리에게 알려 주지 않아요. 계속 재시도하면 행동마다 요청 하나와 기록 한 줄이 끝없이 쌓이게 됩니다.

다시 시작하려면 수신 측을 고친 다음 웹훅을 편집하고 이 주소로 이벤트 전달을 다시 체크하세요. 이를 다시 켜는 것이 카운터를 초기화하므로, 고친 엔드포인트가 실패 한 번만으로 다시 중지되는 일은 없어요.


🧪 테스트 보내기

테스트 전송은 실제로 서명된 요청 하나를 엔드포인트로 즉시 보내고, 상태 코드와 왕복 시간을 보여 줘요. 시뮬레이션이 아니라, 실제 전달이 거치는 것과 같은 코드를 같은 헤더로 거쳐요. 그래서 테스트를 통과한 엔드포인트가 테스트에서 보내지 않은 헤더 때문에 실제 트래픽을 거부하는 일은 없어요.

테스트 본문에는 "event": "panel.webhook.test"와 "category": "test"가 들어 있어서, 수신 측이 알아볼 수 있어요.

한 번이 아니라 두 번 도착할 거예요. 버튼을 누르는 것 자체가 감사 대상이 되는 팀 행동이에요. 그래서 엔드포인트는 테스트 전달을 받고, 팀 카테고리를 구독 중이라면 잠시 뒤 team.webhook.tested에 대한 일반 전달을 하나 더 받아요. 정상이고 루프가 아니에요.

테스트는 20회 실패 한도에 포함되지 않고, 한도를 초기화하지도 않아요. 엔드포인트를 디버깅하다가 중지시키는 일은 없습니다.


📜 최근 시도

최근 시도에는 해당 엔드포인트의 최근 20번의 시도가 표시돼요. 시각, 이벤트, 몇 번째 시도인지, 상태 코드나 오류, 걸린 시간이에요. 실패한 시도에는 서버가 응답한 내용의 짧은 발췌가 함께 표시되는데, 대개 안 된다고 말하는 쪽이 내 코드가 아니라 리버스 프록시라는 사실을 가장 빨리 알아내는 방법이에요.

시도 기록은 30일 동안 보관되고 매일 밤 정리돼요. 이것은 디버깅용 부산물이지 기록이 아니에요. 감사 기록 자체는 훨씬 긴 별도의 보존 기간을 가집니다.


🛡️ 전송을 거부하는 곳

엔드포인트 URL은 공용 인터넷을 가리켜야 해요. 루프백 주소, 사설 대역, 링크 로컬 주소, 클라우드 메타데이터 주소로 해석되는 URL은 거부돼요. 저장할 때, 그리고 매번 전송하기 직전에 다시 확인합니다.

시간이 지날수록 중요한 것은 두 번째 확인이에요. 입력한 날에는 공용 주소를 가리켰던 이름이 한 달 뒤에는 127.0.0.1을 가리킬 수 있고, 웹훅은 존재하는 한 계속 발송되기 때문이에요. 거부는 엔드포인트의 실패로 계산되고 재시도되지 않아요. 같은 질문을 리졸버에게 세 번 더 한다고 달라지는 것은 없으니까요.

https 엔드포인트를 권장해요. 서명은 본문을 누가 보냈는지 증명할 뿐 숨겨 주지는 않아서, 일반 http에서는 활동 내용이 평문으로 전달돼요.


💬 Discord나 Slack 채널로 바로 보내기

엔드포인트에 Discord 웹훅 URL(https://discord.com/api/webhooks/…)을 붙여 넣으면, 패널이 JSON 봉투 대신 Discord 메시지를 보내요. 항목마다 임베드 하나이고, 제목은 설명이며, 이벤트, 카테고리, 실행자, 팀, 서버, 대상, 그리고 있는 경우 변경 내용이 짧은 비교(diff)로 표시돼요. 타임스탬프는 해당 항목의 것이고, 푸터에는 전달 ID가 들어 있어요.

임베드는 위에서 설명한 것과 같은 봉투로 만들어지므로 같은 두 가지 규칙을 따라요. IP 주소는 없고, 인증 정보는 가려져요. 멘션은 Discord 쪽에서 꺼져 있어서, 항목에 @everyone이라는 이름의 채널이나 역할이 있어도 텍스트로만 게시되고 아무도 호출하지 않아요. X-YAWBDB-* 헤더와 서명은 그대로 전송되지만 Discord는 그냥 무시해요.

Discord는 받아들인 메시지에 204 No Content로 응답하고, 최근 시도에서 초록색 행으로 표시되는 것이 바로 이 응답이에요.

Slack 수신 웹훅 URL(https://hooks.slack.com/services/…)도 마찬가지예요. 패널은 항목마다 Slack 메시지 하나를 게시해요. 설명은 굵게, 그 아래에 사실마다 한 줄씩, 변경 내용은 코드 블록으로, 그 밑에 타임스탬프와 전달 ID가 표시돼요. 같은 봉투로 만들어지므로 같은 두 규칙이 적용되고, Slack이 자체 명령용으로 예약한 세 문자(<, >, &)는 이스케이프되어서 멤버가 입력한 이름이 @channel이나 @here로 바뀔 수 없어요. Slack은 받아들인 메시지에 200 ok로 응답해요. Slack의 워크플로와 트리거 URL은 메시지가 아니에요. 이들은 JSON 봉투를 그대로 유지하며, 워크플로가 자체 변수로 풀어서 사용할 수 있어요.


💡 이렇게 활용해요

  • 팀의 활동을 내 Discord나 Slack 채널에 게시하세요. Discord나 Slack 웹훅 URL을 그대로 쓸 수도 있고, 사이에 내 수신 서버를 두어 필터링하고 형식을 바꿀 수도 있어요.
  • 기록을 내 로그 저장소에 미러링해서 패널의 보존 기간이 지난 뒤에도 보관하세요.
  • 업무 시간 외에 모더레이션 조치가 발생하면 누군가를 호출하세요.
  • 기능 설정이 바뀔 때 빌드, 동기화, 백업을 실행하세요.

🔗 함께 보기