웹훅
웹훅은 결제·환불·구독 이벤트를 셀러 서버로 전달해요.
엔드포인트 등록
셀러 콘솔의 외부 연동에서 웹훅 수신 URL을 등록해요.
엔드포인트는 mode별로 등록하며, 이벤트는 같은 mode의 엔드포인트로만 전송돼요.
수신 URL은 공개된 http·https 주소여야 해요.
localhost와 IP 주소는 등록되지 않고, 내부·사설·루프백·링크로컬 등 특수 주소로 해석되는 호스트에는 보내지 않아요.
로컬 개발은 공개 터널 주소를 사용해요.
| 설정 | 설명 |
|---|---|
| mode | live 또는 test 이벤트를 받을 환경이에요. |
| 이벤트 | 필요한 이벤트만 선택해요. 이벤트 목록은 이벤트 레퍼런스를 참고하세요. |
| 상품 범위 | 전체 상품 또는 지정한 상품의 이벤트를 받아요. |
license.issue | 내 서버에서 발급하는 상품의 동기 발급 요청을 받을지 설정해요. 같은 mode와 상품 범위에는 하나만 등록할 수 있어요. 범위가 겹치면 콘솔에서 409 LICENSE_ISSUE_ENDPOINT_CONFLICT로 거절돼요. 내 서버에서 발급을 참고하세요. |
| 시크릿 | 엔드포인트별 whsec_… 값이에요. |
요청 본문
| 항목 | 내용 |
|---|---|
| 공통 필드 | id, type, createdAt, mode, data를 포함해요. |
| mode | 본문과 data의 mode는 모두 live 또는 test예요. |
일반 이벤트의 data | 이벤트 종류에 해당하는 v1 응답 객체 전체예요. 주문 이벤트는 주문 API의 주문 객체와 같은 형태예요. |
license.issue의 data | 발급 요청 전용 구성이며 내 서버에서 발급에서 확인해요. |
{
"id": "evt_01JY8N4X2K7M6Q",
"type": "order.paid",
"createdAt": "2026-10-15T03:20:05.000Z",
"mode": "live",
"data": {
"id": "ord_3kf9a2",
"mode": "live",
"productId": "prod_88x",
"productName": "MeetNote Solo",
"amount": 19000,
"currency": "KRW",
"buyerName": "김구매",
"buyerEmail": "buyer@example.com",
"buyerPhone": null,
"method": "card",
"status": "paid",
"createdAt": "2026-10-15T03:20:00.000Z",
"paidAt": "2026-10-15T03:20:05.000Z",
"refundedAt": null,
"refundedAmount": 0,
"refunds": [],
"confirmedAt": null,
"withdrawalNoticeAgreedAt": null,
"coupon": null,
"subscriptionId": "sub_2kf9a2",
"planId": "plan_monthly",
"billingInterval": "monthly",
"subscriptionEvent": "initial",
"periodStart": "2026-10-15T03:20:05.000Z",
"periodEnd": "2026-11-15T03:20:05.000Z",
"checkoutSessionId": null,
"metadata": null
}
}| 헤더 | 의미 |
|---|---|
webhook-id | 본문 id와 같은 이벤트 ID예요. 재시도와 콘솔 재전송에도 바뀌지 않으며 멱등 키로 사용해요. |
webhook-timestamp | 서명에 포함되는 발송 시각이며, 콘솔에서 다시 보내면 새 값으로 바뀌어요. |
webhook-signature | 원문 본문의 서명이에요. 자세한 내용은 서명 검증을 참고하세요. |
응답과 처리
| 응답 | 판정 |
|---|---|
| 2xx 전체 | 성공이에요. 200만 성공 조건은 아니에요. |
| 3xx 리다이렉트 | 따라가지 않고 실패로 처리해 재시도해요. |
| 2xx 이외 | 실패로 처리해 재시도해요. |
| 10초 초과 또는 네트워크 오류 | 실패로 처리해 재시도해요. |
서명을 검증하고 webhook-id로 중복을 거른 뒤 10초 안에 2xx로 응답해요.
const eventId = req.get('webhook-id')
if (await store.hasProcessed(eventId)) {
return res.sendStatus(202)
}
try {
await queue.publish({ event: req.body, eventId })
} catch {
return res.sendStatus(500) // 2xx를 주지 않아 다시 받아요.
}
await store.markProcessed(eventId)
return res.sendStatus(202)전달 순서와 최신 상태
- 웹훅 전달 순서는 보장되지 않아요.
- 순서대로 처리해야 하면
createdAt을 기준으로 정렬해요. - 중요한 처리 전에는 주문·구독 리소스를 다시 조회해 최신 상태를 확인해요.
테스트 발송
셀러 콘솔에서 webhook.test를 보내 연결·서명을 확인하고, 실제 결제 이벤트가 아니므로 주문·구독 처리에 사용하지 않아요.
Last updated on