Webhooks API
웹훅을 설정하여 주문 상태 변경 등의 이벤트를 실시간으로 수신하는 방법을 안내합니다.
웹훅이란?
웹훅은 특정 이벤트 발생 시 등록된 URL로 HTTP POST 요청을 보내는 방식입니다. 주문 상태를 주기적으로 폴링하지 않아도 실시간으로 변경 사항을 수신할 수 있습니다.
- 이벤트 발생 (주문 결제 완료)
- SweetBook 서버가 등록된 URL로 HTTP POST 전송
- 수신 서버가 200 OK 응답
- 전송 완료 (SUCCESS)
isTest 필드로 Sandbox 이벤트 여부를 구분할 수 있습니다.이벤트 종류
| 이벤트 | 발생 시점 | 활용 예시 |
|---|---|---|
order.created | 주문 생성 (충전금 차감 완료) | 주문 접수 알림, 내부 시스템 동기화 |
order.cancelled | 주문 취소 | 환불 처리, 재고 복원 |
order.restored | 주문 복원 (취소 철회) | 복원된 주문 재처리 |
order.item_cancelled | 주문 항목 부분 취소 | 항목 단위 환불 처리, 남은 항목 기반 재처리 |
production.confirmed | 제작 확정 | 제작 시작 안내 |
production.started | 제작 시작 | 제작 진행 상태 업데이트 |
production.completed | 제작 완료 | 배송 준비 안내 |
shipping.departed | 발송 완료 | 배송 추적 시작, 고객 알림 |
shipping.delivered | 배송 완료 | 배송 완료 안내, 리뷰 요청 |
웹훅 설정하기
1. 웹훅 등록
PUT /webhooks/config로 수신 URL을 등록합니다. 최초 등록 시 secretKey가 자동 생성됩니다.
| 필드 | 필수 | 설명 |
|---|---|---|
webhookUrl | Y | 수신 URL (HTTPS만 허용, 최대 500자) |
events | N | 구독할 이벤트 목록. null이면 전체 이벤트 구독 |
description | N | 설명 (최대 200자) |
whsk_a1b...). 발급 즉시 안전한 곳에 저장하세요.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-Signature | HMAC-SHA256 서명값 (sha256= 접두사 포함) | sha256=a1b2c3d4... |
X-Webhook-Timestamp | 서명 생성 시점 (Unix timestamp, 초) | 1709280000 |
X-Webhook-Event | 이벤트 타입 | order.created |
X-Webhook-Delivery | 전송 고유 ID | wh_abc123xyz |
서명 검증 (HMAC-SHA256)
수신한 웹훅 요청이 SweetBook에서 보낸 것인지 확인하려면 서명을 검증해야 합니다. 서명은 {timestamp}.{payload} 형식의 문자열을 secretKey로 HMAC-SHA256 해시한 값입니다.
서명 페이로드 = "{timestamp}.{JSON body}"
기대값 = "sha256=" + HMAC-SHA256(secretKey, 서명 페이로드)
검증 = 기대값 == X-Webhook-Signature 헤더값 (sha256={hex} 형식)JavaScript (Express.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)
crypto.timingSafeEqual(), Python: hmac.compare_digest()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로 테스트 이벤트를 전송하여 수신 서버가 정상 동작하는지 확인할 수 있습니다. 샘플 데이터가 전송되며, 실제 주문 데이터는 포함되지 않습니다.
responseStatus와 responseBody를 확인하여 문제를 진단하세요.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"}'{
"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로 변경됩니다.
webhook.exhausted 이벤트가 트리거됩니다. 동일 알림은 1시간 쿨다운이 적용되어 과도한 알림이 발생하지 않습니다.전송 상태
| 상태 | 설명 |
|---|---|
PENDING | 전송 대기 또는 진행 중 |
SUCCESS | 전송 성공 (2xx 응답) |
FAILED | 전송 실패 (재시도 대기 중) |
EXHAUSTED | 최대 재시도(3회) 초과 |
전송 이력 확인
GET /webhooks/deliveries로 전송 이력을 조회할 수 있습니다. eventType과 status로 필터링할 수 있습니다.
# 실패한 전송 이력만 조회
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 Request | ERR_VALIDATION_FAILED | webhookUrl 형식 오류, HTTPS 아님, 미지원 eventType 등 |
| 401 Unauthorized | ERR_UNAUTHORIZED | 인증 실패 |
| 404 Not Found | ERR_NOT_FOUND | 등록된 웹훅 설정 없음 (GET/DELETE/POST /webhooks/test) |
| 500 Internal Server Error | ERR_INTERNAL_ERROR | 서버 오류 |
실패 응답 6필드 shape(success·errorCode·message·data·errors·fieldErrors) 및 errorCode 분기 가이드는 에러 코드 & 트러블슈팅을 참고하세요.