워크스페이스 단위 웹훅은 워크스페이스 기본 웹훅입니다.
에이전트에 웹훅이 설정되어 있지 않으면 워크스페이스 웹훅으로 자동 fallback 됩니다.
이 페이지는 워크스페이스 웹훅의 설정 방법과 HMAC 서명 검증 방식을 중심으로 설명합니다.
이벤트 타입과 페이로드 예시는 에이전트 단위 웹훅 문서를 참고하세요.
웹훅 URL 설정
vox.ai는 두 곳에서 웹훅 URL을 설정할 수 있습니다.
| 레벨 | 설정 위치 | 용도 |
|---|
| Workspace | 설정 > 웹훅 | 워크스페이스 기본 웹훅 URL |
| Agent | 에이전트 대시보드 > 웹훅 설정 탭 | 에이전트별 개별 웹훅 URL |
Agent 웹훅 URL이 설정된 경우 해당 URL이 우선 적용됩니다.
미설정 시 Workspace 웹훅 URL로 자동 fallback됩니다.
Transfer Agent로 전환되더라도 최초 인입 Agent의 웹훅 URL이 사용됩니다.
웹훅 URL 등록
대시보드 설정 → 웹훅에서 워크스페이스 웹훅 URL을 입력합니다.
빈 값으로 저장하면 웹훅 전송이 중지됩니다.
서명 키 생성
같은 화면에서 웹훅 서명 키를 생성합니다.
생성된 키는 한 번만 표시되므로 안전한 곳에 저장하세요.
vox.ai는 IP 화이트리스트와 HMAC-SHA256 서명 두 가지 보안 레이어를 제공합니다.
IP 화이트리스트
vox.ai 웹훅 요청은 고정 IP 34.146.189.242에서 전송됩니다.
방화벽 또는 리버스 프록시에서 이 IP만 허용하면 외부 요청을 차단할 수 있습니다.
HMAC-SHA256 서명
웹훅 요청에는 아래 헤더가 포함됩니다.
| 헤더 | 설명 | 예시 |
|---|
X-Webhook-Timestamp | 서명 생성 시점 (Unix, 초) | 1738900000 |
X-Webhook-Signature | HMAC-SHA256 서명 | sha256=a1b2c3... |
서명 계산은 다음 규칙을 따릅니다.
signed_payload = "{timestamp}.{json_body}"
signature = HMAC_SHA256(secret, signed_payload)
json_body는 키 정렬(sort_keys) 및 공백 제거로 직렬화된 문자열이어야 합니다.
normalized_json은 키 정렬 + 공백 없는 JSON입니다.
Python: json.dumps(payload, sort_keys=True, separators=(",", ":"))
JavaScript: JSON.stringify(sortKeys(payload)) (재귀적 키 정렬 필요)
웹훅 서명 키 관리
대시보드의 설정 > 웹훅 메뉴에서 웹훅 서명 키를 생성할 수 있습니다.
키 생성
설정 > 웹훅 페이지에서 키 발급 버튼을 클릭합니다.
키 복사 및 저장
생성된 키를 복사하여 서버의 환경 변수에 저장합니다.# .env
VOX_WEBHOOK_KEY=a3f7c9d2e5b8a1f4c6d9e2b5a8f1c4d7e0b3a6f9c2d5e8b1a4f7c0d3e6b9a2f5
웹훅 서명 키는 생성 직후 한 번만 전체 값이 표시됩니다.
이후에는 마지막 4자리만 확인할 수 있으므로, 반드시 안전한 곳에 저장하세요.
서명 키 재발급 — 기존 키 삭제 후 새 키를 생성합니다. 기존 키로 서명된 웹훅은 즉시 검증 실패합니다.
서명 키 삭제 — HMAC 서명이 비활성화되고, 웹훅 요청에 서명 헤더가 포함되지 않습니다.
서버 구현
HMAC 서명 검증이 포함된 웹훅 엔드포인트 예제입니다.
import hmac, hashlib, json, time, os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_KEY = os.environ["VOX_WEBHOOK_KEY"]
TOLERANCE = 5 * 60 # 5분
def verify_webhook(body: bytes, timestamp: str, signature: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > TOLERANCE:
return False
normalized = json.dumps(json.loads(body), sort_keys=True, separators=(",", ":"))
expected = hmac.new(
secret.encode(), f"{timestamp}.{normalized}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature.removeprefix("sha256="))
@app.post("/webhook")
async def handle_webhook(request: Request):
body = await request.body()
ts = request.headers.get("X-Webhook-Timestamp", "")
sig = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(body, ts, sig, WEBHOOK_KEY):
raise HTTPException(status_code=401)
data = json.loads(body)
# 페이로드 처리 ...
서명 검증 후에는 HTTP 200을 빠르게 반환하세요.
업스트림 처리(저장/분석 등)는 비동기 작업으로 분리하는 것을 권장합니다.
보안 권장사항
두 레이어 모두 사용
IP 화이트리스트 + HMAC 서명을 함께 사용하면 네트워크와 애플리케이션 레벨 모두에서 보안을 확보할 수 있습니다.
Timestamp 검증
타임스탬프가 현재 시간과 5분 이내인지 확인하여 Replay 공격을 방지합니다.
Timing-safe 비교
hmac.compare_digest (Python) 또는 crypto.timingSafeEqual (Node.js)로 Timing 공격을 방지합니다.
키 안전 보관
웹훅 서명 키는 환경 변수 또는 시크릿 매니저에 저장하고, 코드에 하드코딩하지 않습니다.