Webhooks API

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

웹훅이란?

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

  1. 이벤트 발생 (주문 결제 완료)
  2. SweetBook 서버가 등록된 URL로 HTTP POST 전송
  3. 수신 서버가 200 OK 응답
  4. 전송 완료 (SUCCESS)
Sandbox 환경: 웹훅 이벤트는 Live와 Sandbox 환경 모두에서 발생합니다. 페이로드의 isTest 필드로 Sandbox 이벤트 여부를 구분할 수 있습니다.

이벤트 종류

이벤트발생 시점활용 예시
order.created주문 생성 (충전금 차감 완료)주문 접수 알림, 내부 시스템 동기화
order.cancelled주문 취소환불 처리, 재고 복원
order.restored주문 복원 (취소 철회)복원된 주문 재처리
order.item_cancelled주문 항목 부분 취소항목 단위 환불 처리, 남은 항목 기반 재처리
production.confirmed제작 확정제작 시작 안내
production.started제작 시작제작 진행 상태 업데이트
production.completed제작 완료배송 준비 안내
shipping.departed발송 완료배송 추적 시작, 고객 알림
shipping.delivered배송 완료배송 완료 안내, 리뷰 요청
각 이벤트의 페이로드 구조와 JSON 예시는 Webhook Events 레퍼런스를 참고하세요.

웹훅 설정하기

1. 웹훅 등록

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

필드필수설명
webhookUrlY수신 URL (HTTPS만 허용, 최대 500자)
eventsN구독할 이벤트 목록. null이면 전체 이벤트 구독
descriptionN설명 (최대 200자)
secretKey는 최초 등록 시에만 전체 값이 반환됩니다. 이후 조회 시에는 앞 8자만 노출됩니다 (예: whsk_a1b...). 발급 즉시 안전한 곳에 저장하세요.
Request
curl -X PUT 'https://api-sandbox.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.created", "shipping.departed"],
  "description": "주문 알림 수신"
}'

2. 설정 조회/수정

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

3. 웹훅 해제

DELETE /webhooks/config로 웹훅을 비활성화합니다. 기존 전송 이력은 유지됩니다.

전송 헤더

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

헤더설명예시
X-Webhook-SignatureHMAC-SHA256 서명값 (sha256= 접두사 포함)sha256=a1b2c3d4...
X-Webhook-Timestamp서명 생성 시점 (Unix timestamp, 초)1709280000
X-Webhook-Event이벤트 타입order.created
X-Webhook-Delivery전송 고유 IDwh_abc123xyz

서명 검증 (HMAC-SHA256)

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

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

JavaScript (Express.js)

Node.js
const crypto = require('crypto');

function verifySignature(payload, signature, timestamp, secretKey) {
  const signPayload = `${timestamp}.${payload}`;
  const expectedHex = crypto
    .createHmac('sha256', secretKey)
    .update(signPayload)
    .digest('hex');
  const expected = `sha256=${expectedHex}`;
  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.created':
      console.log('주문 생성 완료:', req.body);
      break;
    case 'shipping.departed':
      console.log('발송 완료:', req.body);
      break;
  }

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

Python (Flask)

서명 비교 시 반드시 타이밍 공격을 방지하는 함수를 사용하세요. JavaScript: crypto.timingSafeEqual(), Python: hmac.compare_digest()
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_hex = hmac.new(
        secret_key.encode(),
        sign_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    expected = f"sha256={expected_hex}"
    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.created':
        print(f"주문 생성 완료: {data}")
    elif event == 'shipping.departed':
        print(f"발송 완료: {data}")

    return jsonify({"received": True}), 200

테스트하기

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

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

재시도 정책

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

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

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

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

전송 상태

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

전송 이력 확인

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

Request
# 실패한 전송 이력만 조회
curl 'https://api-sandbox.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만 허용됩니다.

HTTP 상태 코드

Webhooks API의 5개 엔드포인트(PUT /webhooks/config, GET /webhooks/config, DELETE /webhooks/config, POST /webhooks/test, GET /webhooks/deliveries)에서 반환되는 상태 코드입니다.

코드errorCode설명
200 OK조회/수정/삭제/테스트 성공
201 Created웹훅 최초 등록 성공 (PUT /webhooks/config)
400 Bad RequestERR_VALIDATION_FAILEDwebhookUrl 형식 오류, HTTPS 아님, 미지원 eventType
401 UnauthorizedERR_UNAUTHORIZED인증 실패
404 Not FoundERR_NOT_FOUND등록된 웹훅 설정 없음 (GET/DELETE/POST /webhooks/test)
500 Internal Server ErrorERR_INTERNAL_ERROR서버 오류

실패 응답 6필드 shape(success·errorCode·message·data·errors·fieldErrors) 및 errorCode 분기 가이드는 에러 코드 & 트러블슈팅을 참고하세요.