Webhooks 연동 가이드

웹훅을 설정하여 주문 상태 변경 등의 이벤트를 실시간으로 수신하는 방법을 안내합니다.

웹훅이란?

웹훅은 특정 이벤트 발생 시 등록된 URL로 HTTP POST 요청을 보내는 방식입니다. 주문 상태를 주기적으로 폴링하지 않아도 실시간으로 변경 사항을 수신할 수 있습니다.

text
이벤트 발생 (주문 결제 완료)
  → SweetBook 서버가 등록된 URL로 HTTP POST 전송
  → 수신 서버가 200 OK 응답
  → 전송 완료 (SUCCESS)

Sandbox 환경: 웹훅 이벤트는 Live와 Sandbox 환경 모두에서 발생합니다. 페이로드의 isTest 필드로 Sandbox 이벤트 여부를 구분할 수 있습니다.

이벤트 종류

이벤트발생 시점활용 예시
order.paid주문 생성 (충전금 차감 완료)주문 접수 알림, 내부 시스템 동기화
order.confirmed제작 확정제작 시작 안내
order.status_changed주문 상태 변경상태 이력 추적, 대시보드 업데이트
order.shipped발송 완료배송 추적 시작, 고객 알림
order.cancelled주문 취소환불 처리, 재고 복원
각 이벤트의 페이로드 구조와 JSON 예시는 Webhook Events 레퍼런스를 참고하세요.

웹훅 설정하기

1. 웹훅 등록

PUT /webhooks/config로 수신 URL을 등록합니다. 최초 등록 시 secretKey가 자동 생성됩니다.

bash
curl -X PUT 'https://api.sweetbook.com/v1/webhooks/config' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "webhookUrl": "https://example.com/webhooks/sweetbook",
  "events": ["order.paid", "order.shipped"],
  "description": "주문 알림 수신"
}'
필드필수설명
webhookUrlY수신 URL (HTTPS만 허용, 최대 500자)
eventsN구독할 이벤트 목록. null이면 전체 이벤트 구독
descriptionN설명 (최대 200자)
secretKey는 최초 등록 시에만 전체 값이 반환됩니다. 이후 조회 시에는 앞 8자만 노출됩니다 (예: whsk_a1b...). 발급 즉시 안전한 곳에 저장하세요.

2. 설정 조회/수정

GET /webhooks/config로 현재 설정을 확인할 수 있습니다. 설정을 변경하려면 PUT /webhooks/config를 다시 호출하면 됩니다. 기존 secretKey는 유지되며, 수정 응답에는 전체 secretKey가 포함됩니다.

3. 웹훅 해제

DELETE /webhooks/config로 웹훅을 비활성화합니다. 소프트 삭제로 처리되며 전송 이력은 유지됩니다.


전송 헤더

웹훅 요청에는 다음 4개의 헤더가 포함됩니다.

헤더설명예시
X-Webhook-SignatureHMAC-SHA256 서명값 (hex)a1b2c3d4...
X-Webhook-Timestamp서명 생성 시점 (Unix timestamp, 초)1709280000
X-Webhook-Event이벤트 타입order.paid
X-Webhook-Delivery전송 고유 IDwh_abc123xyz

서명 검증 (HMAC-SHA256)

수신한 웹훅 요청이 SweetBook에서 보낸 것인지 확인하려면 서명을 검증해야 합니다. 서명은 {timestamp}.{payload} 형식의 문자열을 secretKey로 HMAC-SHA256 해시한 값입니다.

text
서명 페이로드 = "{timestamp}.{JSON body}"
기대값 = HMAC-SHA256(secretKey, 서명 페이로드)
검증 = 기대값 == X-Webhook-Signature 헤더값

JavaScript (Express.js)

javascript
const crypto = require('crypto');

function verifySignature(payload, signature, timestamp, secretKey) {
  const signPayload = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac('sha256', secretKey)
    .update(signPayload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

app.post('/webhooks/sweetbook', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const timestamp = req.headers['x-webhook-timestamp'];
  const event = req.headers['x-webhook-event'];
  const payload = JSON.stringify(req.body);

  if (!verifySignature(payload, signature, timestamp, SECRET_KEY)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 이벤트 처리
  switch (event) {
    case 'order.paid':
      console.log('주문 결제 완료:', req.body);
      break;
    case 'order.shipped':
      console.log('발송 완료:', req.body);
      break;
  }

  res.status(200).json({ received: true });
});

Python (Flask)

python
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET_KEY = "whsk_your_secret_key"

def verify_signature(payload, signature, timestamp, secret_key):
    sign_payload = f"{timestamp}.{payload}"
    expected = hmac.new(
        secret_key.encode(),
        sign_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

@app.route('/webhooks/sweetbook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    event = request.headers.get('X-Webhook-Event')
    payload = request.get_data(as_text=True)

    if not verify_signature(payload, signature, timestamp, SECRET_KEY):
        return jsonify({"error": "Invalid signature"}), 401

    # 이벤트 처리
    data = request.get_json()
    if event == 'order.paid':
        print(f"주문 결제 완료: {data}")
    elif event == 'order.shipped':
        print(f"발송 완료: {data}")

    return jsonify({"received": True}), 200
서명 비교 시 반드시 타이밍 공격을 방지하는 함수를 사용하세요. JavaScript: crypto.timingSafeEqual(), Python: hmac.compare_digest()

테스트하기

POST /webhooks/test로 테스트 이벤트를 전송하여 수신 서버가 정상 동작하는지 확인할 수 있습니다. 샘플 데이터가 전송되며, 실제 주문 데이터는 포함되지 않습니다.

bash
curl -X POST 'https://api.sweetbook.com/v1/webhooks/test' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"eventType": "order.paid"}'
json
{
  "success": true,
  "message": "Success",
  "data": {
    "deliveryUid": "wh_abc123xyz",
    "eventType": "order.paid",
    "status": "SUCCESS",
    "responseStatus": 200,
    "responseBody": "{\"received\": true}"
  }
}
테스트 전송은 재시도되지 않습니다. 실패 시 응답에서 responseStatusresponseBody를 확인하여 문제를 진단하세요.

재시도 정책

전송 실패(2xx 외 응답 또는 타임아웃) 시 자동으로 최대 3회 재시도합니다.FAILED 상태인 건만 재시도 대상이며, PENDING 상태는 재시도되지 않습니다. HTTP 타임아웃은 30초입니다. 테스트 전송(is_test=true)은 재시도 없이 1회만 전송됩니다.

시도대기 시간
1차 재시도1분 후
2차 재시도5분 후
3차 재시도30분 후

3회 모두 실패하면 상태가 EXHAUSTED로 변경됩니다.

EXHAUSTED 알림: 모든 재시도가 소진되면 webhook.exhausted 이벤트가 트리거됩니다. 동일 알림은 1시간 쿨다운이 적용되어 과도한 알림이 발생하지 않습니다.

전송 상태

상태설명
PENDING전송 대기 또는 진행 중
SUCCESS전송 성공 (2xx 응답)
FAILED전송 실패 (재시도 대기 중)
EXHAUSTED최대 재시도(3회) 초과

전송 이력 확인

GET /webhooks/deliveries로 전송 이력을 조회할 수 있습니다. eventTypestatus로 필터링할 수 있습니다.

bash
# 실패한 전송 이력만 조회
curl 'https://api.sweetbook.com/v1/webhooks/deliveries?status=FAILED&limit=10' \
  -H 'Authorization: Bearer YOUR_API_KEY'

수신 서버 구현 시 유의사항

  • 수신 서버는 200~299 상태 코드로 응답해야 합니다. 그 외 응답은 실패로 처리됩니다.
  • 30초 이내에 응답해야 합니다. 시간이 오래 걸리는 작업은 비동기로 처리하세요.
  • X-Webhook-Signature를 반드시 검증하여 위변조 요청을 차단하세요.
  • 같은 이벤트가 중복 전송될 수 있습니다. X-Webhook-Delivery 헤더로 중복 여부를 확인하세요.
  • 수신 URL은 HTTPS만 허용됩니다.

관련 문서