웹훅 — 개요
웹훅은 권위 있는 신호입니다. 브라우저 콜백(onSuccess)과 대시보드 뷰는
편의 사항이며, 웹훅이 그라운드 트루스입니다.
전달 보장
- At least once. 단일 이벤트는 가맹점 서버가 타임아웃 내에 2xx를
반환하지 않으면 최대 6회까지 전달될 수 있습니다. 핸들러를 멱등성
있게 만드세요 —
X-Delivery로 중복을 제거하세요(페이로드에는event_id필드가 없으며, 안정적인 전달 UUID가 멱등성 키입니다). - HTTP 요청당 하나의 이벤트. 배치 없음.
- 엔드포인트별 격리. 여러 엔드포인트가 등록되어 있다면 각각 자체 전달 + 재시도 트랙을 가집니다. 느린 가맹점 URL 하나가 다른 것을 굶주리게 할 수 없습니다 — 각 호스트는 자체 서킷 브레이커를 가집니다.
- 서명됨. 모든 페이로드는
X-Signature헤더(그리고 로테이션 후 24시간 윈도우 동안X-Signature-Prev도)를 가집니다. 본문으로 무엇이든 하기 전에 검증하세요. 서명 검증을 참조하세요.
구독 가능한 이벤트 타입
| 이벤트 | 발생 시점 |
|---|---|
payment.settled | 온체인 송금이 체인의 확인 수를 클리어함. 이것을 사용하여 주문을 결제 완료로 표시하세요. |
payment.failed | 법정화폐 결제가 제공자에 의해 명시적으로 거부됨(현재: 실패를 시그널링하는 Stripe 웹훅). 암호화폐 타임아웃에는 발생하지 않음 — 그것은 대신 checkout.expired로 노출되고, 부족한 암호화폐 결제는 payment.underpaid로 노출됩니다. |
payment.underpaid | 자금이 도착했지만 주문 총액에 부족함(일반적: 금액에서 스테이블코인 송금 수수료가 차감됨). |
payment.overpaid | 자금이 주문 총액을 초과하여 도착함. 잉여는 기록되지만 자동 환불되지 않습니다. |
order.created | 새 주문이 열림 — B2B API 호출 또는 체크아웃 세션 전환에 의해. |
order.canceled | 주문이 취소됨. 페이로드의 data.reason이 수동 취소와 payment_timeout(워커가 스윕한 오래된 미결제 주문)을 구분합니다. |
order.resolved | PARTIAL_PAID 주문이 PAID로 해결됨 — 가맹점이 부족분을 수용함. |
order.reopened | 이전에 자동 취소된 주문(canceled_reason=payment_timeout)이 가맹점에 의해 재오픈됨. |
checkout.created | 구매자가 주문의 체크아웃을 열었음. |
checkout.completed | 구매자 측 흐름이 완료됨(온체인 정산을 의미하지 않음 — 그것은 payment.settled 사용). |
checkout.expired | 구매자가 포기했고 세션 TTL이 끝남. |
payment.refund.requested | 환불 기록이 생성됨 — 가맹점 시작 API 호출 또는 고객 제출 환불 요청 양식으로부터. |
payment.refund.approved | 대기 중인 환불이 승인 워크플로우를 통과함. |
payment.refund.rejected | 대기 중인 환불이 거부됨. |
payment.refund.executed | 환불의 온체인 송금이 클리어되어 기록이 종료 상태 executed로 이동함. |
refund_request.created | 환불 요청 토큰이 발급됨. data.source는 b2b / dashboard / renewal. 구독 선택 사항 — 주문별로 현재 활성화된 토큰을 추적하는 감사 파이프라인에 유용합니다. |
refund_request.renewal_requested | 구매자가 토큰 만료 후 “새 링크 요청”을 클릭함. 구독을 강력히 권장합니다 — 갱신 위젯에 처리할 새 항목이 있음을 가맹점에 알리는 신호입니다. |
refund_request.renewed | 갱신이 승인되어 새 토큰이 이전 토큰을 대체함. data.old_token / data.new_token이 감사 체인을 형성합니다. |
refund_request.canceled | 가맹점이 대시보드에서 토큰을 CANCELED로 전환함(예: 갱신 요청 거부, 라이브 링크 종료). 멱등적 — 첫 전이만 발생합니다. data.reason은 선택적 가맹점 메모입니다. |
대시보드는 이 목록을 GET /v1/webhooks/event-types에서 가져오므로 엔드포인트
생성/편집 양식은 항상 플랫폼이 실제로 발생시키는 것과 일치합니다. 저희가
출시하지 않는 이벤트를 구독하면 생성 시 명확한 오류로 거부됩니다.
테스트 이벤트는 구독할 수 없습니다. 대시보드의 엔드포인트별 Send Test
버튼은 해당 엔드포인트 하나에만 webhook.test.ping envelope를 동기적으로
POST하고(재시도 파이프라인 우회), 레거시 가맹점 레벨 “Send test event”
경로는 필터와 관계없이 모든 활성 엔드포인트로 webhook.test envelope를
팬아웃합니다. 위 카탈로그에는 둘 다 나타나지 않습니다 — 등록된 엔드포인트가
있다는 이유로 받는 것이며, 구독에 의한 것이 아닙니다.
처리하는 이벤트만 구독하세요. 각 엔드포인트는 자체 이벤트 필터를
가집니다. 와일드카드 "*"는 “미래에 추가될 것을 포함한 모든 이벤트”를
의미합니다. 더 적은 이벤트를 구독하면 핸들러가 더 깔끔해지고 동시에
오류 시 재시도해야 하는 표면적이 줄어듭니다.
페이로드 + 헤더
HTTP 본문은 이벤트별 데이터 객체 자체입니다. Stripe 스타일의 외부
envelope 없음 — 이벤트 타입, 전달 ID, 발신 타임스탬프와 같은 필드는
헤더에 있습니다. payment.settled의 경우 본문은 다음과 같습니다.
{
"receipt_id": "rcp_…",
"order_id": "ord_…",
"payment_intent_id": "pin_…",
"checkout_session_id": "cst_…",
"merchant_id": "mer_…",
"customer_id": "cus_…",
"total": "49.00",
"currency": "USD",
"payment_method": "crypto",
"token": "USDC",
"network": "polygon",
"tx_hash": "0x…",
"deposit_address": "0x…",
"treasury_address": "0x…",
"amount_received": "49.00",
"confirmations": 5,
"metadata": { /* 이벤트별 */ }
}다른 이벤트는 자체 필드 세트를 가집니다 — 이벤트별 문서가 출시되기 전까지
정규 형식은 payment-service/internal/domain/events.go의 publisher 구조체를
참조하세요. 필드 이름은 안정적입니다(소문자 스네이크 케이스); 온체인 tx
해시는 항상 tx_hash(transaction_hash가 아님)입니다.
인바운드 요청의 헤더
Content-Type: application/json
X-Event: payment.settled
X-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Timestamp: 1729536000
X-Signature: sha256=9a8b7c…
X-Signature-Prev: sha256=fa31b2… (로테이션 grace 윈도우 동안에만)| 헤더 | 내용 |
|---|---|
X-Event | 이벤트 타입(예: payment.settled). JSON 파싱을 건너뛰려면 프록시 레이어에서 이것으로 라우팅하세요. |
X-Delivery | 전달 행을 식별하는 UUID. 동일한 (event, endpoint) 쌍의 모든 재시도에서 안정적 — 멱등성 키로 사용하세요. |
Idempotency-Key | X-Delivery와 미러링(동일한 값). 모든 전달에 설정됨 — Stripe / GitHub 관례를 따릅니다. |
X-Timestamp | 시도가 전송된 유닉스 초. 페이로드에 서명되므로 캡처된 (body, X-Signature) 쌍이 무한정 재생될 수 없습니다 — 타임스탬프가 허용 윈도우 밖인 전달을 거부하세요. |
X-Signature | HMAC-SHA256(secret, X-Timestamp + "." + raw_body)의 sha256=<hex>. 서명 검증 참조. |
X-Signature-Prev | 이전 시크릿으로 동일한 알고리즘. 로테이션 후 24시간 윈도우에만 존재 — 어느 쪽 키를 실행 중인 검증자도 컷오버 동안 전달을 계속 받을 수 있게 합니다. 윈도우가 닫힌 후 헤더는 더 이상 전송되지 않습니다. |
재시도 스케줄
엔드포인트가 타임아웃 내에 2xx를 반환하지 않으면 다음 스케줄로 재시도합니다
(타임스탬프는 첫 시도 기준).
| 시도 | 지연 | 누적 |
|---|---|---|
| 1 | 0s | 0s |
| 2 | +1 min | 1m |
| 3 | +5 min | 6m |
| 4 | +15 min | 21m |
| 5 | +1 hour | 1h 21m |
| 6 | +6 hours | 7h 21m |
6번째 시도가 실패하면 전달은 데드 레터로 이동되고 가맹점 계정 이메일에
알림이 전송됩니다. 데드 레터링된 이벤트는 대시보드의 Developers →
Webhooks → Delivery history 패널에서 또는
POST /v1/webhooks/deliveries/:id/replay로 직접 재생할 수 있습니다. 각
재생은 자체 X-Delivery로 새 전달 행을 생성합니다 — 감사 체인은
parent_delivery_id를 통해 원본과 연결되므로 재생의 재시도가 소스 이벤트를
가리지 않습니다.
엔드포인트 등록
가맹점 대시보드 에서:
- Developers → Webhooks → + Add endpoint
- URL 붙여넣기 —
https://…만(일반 HTTP는 거부됨; 생성 양식도localhost, 사설 IP 범위, userinfo가 있는 URL을 차단함) - 구독할 이벤트 선택(또는
*모두) - 환경 선택 — test 또는 live(각각 자체 시크릿을 가지며 절대 교차되지 않음)
- 저장 → 대시보드가 서명 시크릿(
whsec_…)을 한 번 표시합니다. 서버 측에 저장하세요 — 다음 두 기능에 필요합니다.
가맹점당 환경당 최대 10개의 엔드포인트(예: 프로덕션 이행용 하나, 스테이징 미러링용 하나, Slack 알림용 하나)를 등록할 수 있습니다. 각각 자체 재시도 상태, 시크릿, 호스트별 서킷 브레이커를 유지합니다.
각 엔드포인트의 라이프사이클 액션
각 엔드포인트 카드의 ⋮ 메뉴는 다음을 노출합니다.
- Edit — URL, 설명 또는 구독 목록 변경. 새 URL은 생성과 동일한
https:///SSRF 규칙으로 재검증됩니다. - Send Test — 현재 시크릿으로 서명된
webhook.test.pingenvelope를 동기적으로 POST합니다. 대시보드는 HTTP 상태, 지연, 응답의 512바이트 스니펫을 표시합니다. RMQ 파이프라인을 우회하여 답변이 즉시 반환됩니다. - Rotate Secret — 새 시크릿을 생성합니다. 이전 시크릿은 24시간
동안 유효합니다(전달은
X-Signature와X-Signature-Prev를 모두 포함하여 윈도우 동안 어느 쪽 키를 실행 중인 검증자도 재배포 중에 이벤트를 계속 받을 수 있게 합니다). - Reveal Secret — 기존 시크릿을 다시 표시합니다. 새 2FA 검증으로 게이팅되고 감사 로그에 기록됨. 사본을 분실했고 Rotate가 허용되지 않는 경우에만 사용하세요.
- Enable / Disable — 전달 이력을 잃지 않고
is_active를 토글합니다. 비활성화된 엔드포인트는 대시보드에 남아 있지만 새 전달을 받지 않습니다. - Delete — 영구적입니다. 나중에 재활성화할 수 있는 경우 Disable을 사용하세요.
핸들러를 위한 팁
- 빠르게 2xx 반환. 무거운 작업을 수행하기 전에
200 OK로 응답하세요 — 이행을 백그라운드 큐로 보내세요. 시도당 타임아웃은 10초입니다. 더 오래 응답을 보류하면 재시도가 트리거됩니다. 타임아웃은 플랫폼 측이며 가맹점이 구성할 수 없습니다 — 핸들러가 진정으로 더 많은 시간이 필요한 경우 지원팀에 문의하세요. X-Delivery로 중복 제거(또는Idempotency-Key— 동일한 값). 2xx를 반환해도 업스트림 프록시가 연결을 끊고 재시도를 트리거할 수 있습니다. 전달 ID는 동일한 전달 행의 모든 재시도에서 안정적이므로 올바른 키입니다.- 알 수 없는 이벤트 타입을 허용. 새 이벤트가 나타날 수 있습니다. 4xx 대신 200을 반환하고 no-op 하세요. 그렇지 않으면 재시도 큐가 채워집니다.
- 비즈니스 로직 옆에
X-Delivery로깅. 무언가 잘못되면 그것이 저희 측과 가맹점 측 사이의 조인 키입니다.