Skip to Content
개발자웹훅개요

웹훅

웹훅은 결제·환불·구독 이벤트를 셀러 서버로 전달해요.

엔드포인트 등록

셀러 콘솔의 외부 연동에서 웹훅 수신 URL을 등록해요.
엔드포인트는 mode별로 등록하며, 이벤트는 같은 mode의 엔드포인트로만 전송돼요.
수신 URL은 공개된 http·https 주소여야 해요.
localhost와 IP 주소는 등록되지 않고, 내부·사설·루프백·링크로컬 등 특수 주소로 해석되는 호스트에는 보내지 않아요.
로컬 개발은 공개 터널 주소를 사용해요.

설정설명
modelive 또는 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