Step 6: 웹훅으로 상태 추적

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

웹훅 설정

PUT /webhooks/config로 웹훅 URL과 수신할 이벤트를 등록합니다.

bash
curl -X PUT 'https://api-sandbox.sweetbook.com/v1/webhooks/config' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://your-server.com/webhooks/sweetbook",
  "events": [
    "order.created",
    "production.confirmed",
    "production.started",
    "production.completed",
    "shipping.departed",
    "order.cancelled"
  ]
}'

설정 조회

bash
curl -X GET 'https://api-sandbox.sweetbook.com/v1/webhooks/config' \
  -H 'Authorization: Bearer YOUR_API_KEY'

테스트 이벤트 전송

POST /webhooks/test로 등록된 URL에 테스트 이벤트를 보내 정상 수신 여부를 확인할 수 있습니다.

bash
curl -X POST 'https://api-sandbox.sweetbook.com/v1/webhooks/test' \
  -H 'Authorization: Bearer YOUR_API_KEY'

서명 검증 (HMAC-SHA256)

웹훅 요청의 진위를 확인하기 위해 HMAC-SHA256 서명을 검증해야 합니다. 요청 헤더에 다음 정보가 포함됩니다.

헤더설명
X-Webhook-Event이벤트 타입 (예: order.created)
X-Webhook-Delivery고유 전송 ID
X-Webhook-TimestampUnix 타임스탬프
X-Webhook-Signature서명 값 (sha256=... 형식)

서명 검증 방식: {timestamp}.{payload} 문자열을 시크릿 키로 HMAC-SHA256 해싱하여 비교합니다.

JavaScript 예시

javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, headers, secret) {
  const timestamp = headers['x-webhook-timestamp'];
  const signature = headers['x-webhook-signature'];

  const expectedSig = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payload}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSig)
  );
}

// Express.js 예시
app.post('/webhooks/sweetbook', express.raw({ type: 'application/json' }), (req, res) => {
  const payload = req.body.toString();
  const isValid = verifyWebhookSignature(payload, req.headers, WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);
  console.log('Event:', event.event_type, event.data);

  res.status(200).send('OK');
});

Python 예시

python
import hmac
import hashlib

def verify_webhook_signature(payload: bytes, headers: dict, secret: str) -> bool:
    timestamp = headers.get('X-Webhook-Timestamp', '')
    signature = headers.get('X-Webhook-Signature', '')

    message = f"{timestamp}.{payload.decode('utf-8')}"
    expected_sig = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_sig)

# Flask 예시
@app.route('/webhooks/sweetbook', methods=['POST'])
def handle_webhook():
    payload = request.get_data()
    if not verify_webhook_signature(payload, request.headers, WEBHOOK_SECRET):
        return 'Invalid signature', 401

    event = request.get_json()
    print(f"Event: {event['event_type']}", event['data'])

    return 'OK', 200

공통 페이로드 구조

모든 웹훅 이벤트는 다음과 같은 공통 구조로 전달됩니다.

json
{
  "event_uid": "evt_abc123def456",
  "event_type": "order.created",
  "created_at": "2025-10-21T03:00:00Z",
  "data": {
    ...
  }
}
필드설명
event_uid이벤트 고유 ID
event_type이벤트 타입
created_at이벤트 생성 시각 (ISO 8601)
data이벤트별 상세 데이터

웹훅 이벤트 목록

1. order.created — 주문 생성/결제완료

json
{
  "event_uid": "evt_abc123def456",
  "event_type": "order.created",
  "created_at": "2025-10-21T03:00:00Z",
  "data": {
    "order_uid": "ord_a1b2c3d4e5f6",
    "order_status": "PAID",
    "total_amount": 15500,
    "item_count": 1,
    "ordered_at": "2025-10-21T03:00:00Z"
  }
}

2. production.confirmed — 제작 확정

json
{
  "event_uid": "evt_def456ghi789",
  "event_type": "production.confirmed",
  "created_at": "2025-10-22T09:00:00Z",
  "data": {
    "order_uid": "ord_a1b2c3d4e5f6",
    "order_status": "CONFIRMED",
    "print_day": "2025-10-23",
    "confirmed_at": "2025-10-22T09:00:00Z"
  }
}

3. production.started — 제작 시작

json
{
  "event_uid": "evt_ghi789jkl012",
  "event_type": "production.started",
  "created_at": "2025-10-23T08:00:00Z",
  "data": {
    "order_uid": "ord_a1b2c3d4e5f6",
    "order_status": "IN_PRODUCTION",
    "print_day": "2025-10-23",
    "started_at": "2025-10-23T08:00:00Z"
  }
}

4. production.completed — 제작 완료

json
{
  "event_uid": "evt_jkl012mno345",
  "event_type": "production.completed",
  "created_at": "2025-10-25T16:00:00Z",
  "data": {
    "order_uid": "ord_a1b2c3d4e5f6",
    "order_status": "PRODUCTION_COMPLETE",
    "completed_at": "2025-10-25T16:00:00Z"
  }
}

5. shipping.departed — 배송 출발

json
{
  "event_uid": "evt_mno345pqr678",
  "event_type": "shipping.departed",
  "created_at": "2025-10-26T10:00:00Z",
  "data": {
    "order_uid": "ord_a1b2c3d4e5f6",
    "item_uid": "itm_x1y2z3w4",
    "tracking_number": "1234567890",
    "tracking_carrier": "한진택배",
    "shipped_at": "2025-10-26T10:00:00Z"
  }
}

6. order.cancelled — 주문 취소

json
{
  "event_uid": "evt_pqr678stu901",
  "event_type": "order.cancelled",
  "created_at": "2025-10-21T04:00:00Z",
  "data": {
    "order_uid": "ord_a1b2c3d4e5f6",
    "order_status": "CANCELLED_REFUND",
    "cancel_reason": "고객 요청에 의한 취소",
    "refund_amount": 15500,
    "cancelled_at": "2025-10-21T04:00:00Z"
  }
}

주문 상태 ↔ 웹훅 이벤트 매핑

주문 상태웹훅 이벤트설명
PAIDorder.created주문 생성 / 결제완료
CONFIRMEDproduction.confirmed제작 확정
IN_PRODUCTIONproduction.started제작 시작
PRODUCTION_COMPLETEproduction.completed제작 완료
SHIPPEDshipping.departed배송 출발 (송장번호 포함)
CANCELLED_REFUNDorder.cancelled주문 취소 및 환불

재시도 정책

웹훅 전송이 실패하면 (비-2xx 응답 또는 타임아웃) 최대 3회까지 자동으로 재시도합니다.

시도대기 시간설명
1차 재시도1분 후첫 번째 실패 후
2차 재시도5분 후두 번째 실패 후
3차 재시도30분 후세 번째 실패 후 (최종)
3회 재시도 후에도 전송에 실패하면 해당 이벤트는 더 이상 전송되지 않습니다. 파트너 포털의 웹훅 로그에서 실패 내역을 확인할 수 있습니다.

폴링 대안

웹훅 구현이 어려운 경우 GET /orders/{orderUid}로 주문 상태를 주기적으로 조회하는 폴링 방식을 사용할 수 있습니다.

bash
curl -X GET 'https://api-sandbox.sweetbook.com/v1/orders/{orderUid}' \
  -H 'Authorization: Bearer YOUR_API_KEY'
폴링 시 Rate Limit을 고려하여 적절한 간격(예: 5~10분)을 두고 조회하세요. 실시간성이 중요한 경우 웹훅 사용을 권장합니다.

다음 단계