웹훅 이벤트
1. 이벤트 한눈에 보기
이벤트 타입 | 의미 |
|---|---|
| 일반결제 완료 |
| 일반결제 취소 요청 |
| 일반결제 취소 완료 |
| 정기결제 완료 |
| 정기결제 취소 완료 (회차 단위) |
| 정기결제 갱신 실패 |
| 정기결제 해지 요청 |
| 정기결제 해지 완료 |
이름 규칙
subscription_payment.*는 회차 결제 1건에 대한 이벤트입니다: 완료(completed) · 취소(refunded) · 실패(failed).subscription.*는 정기결제 자체의 상태 변화입니다: 해지 요청(cancel_requested) · 해지 완료(terminated).그래서 회차 하나가 취소되어도(
subscription_payment.refunded) 정기결제는 계속 유지될 수 있고, 정기결제가 끝나는 이벤트는subscription.terminated로 따로 전달됩니다.
공통 규칙
envelope 공통 필드
id,type,version,occurredAt은 연동 가이드의 "요청 형식"을 기준으로 봅니다.아래 JSON 예시는 테스트 발송 payload 형식 기준입니다.
id와 시각 값은 발송 시점마다 달라집니다.값이 없으면 키를 생략하는 것이 기본입니다.
금액은 모두 원(KRW) 단위 정수입니다.
시각은 모두 ISO-8601 KST(
+09:00)입니다.options[]는 단일 옵션이어도 항상 배열입니다.enum 값은 실제 payload 원문을 그대로 적고, 필요한 곳에는 한글 의미를 함께 풀어 적었습니다.
표의
타입은 수신 파서 기준으로string,number,boolean,object,array만 사용합니다. 날짜와 시각 형식은 의미 칸에서 함께 설명합니다.필드는 앞으로도 추가될 수 있습니다. 수신 파서는
data.object의 모르는 키를 무시하도록(tolerant parsing) 작성해 주세요.
읽는 방법
payment.completed를 기준 스키마로 보면 됩니다.나머지 7개 이벤트는 각 섹션의 "대비 차이" 표에 적힌 필드만 확인하면 됩니다. 표에 없는 필드는 비교 대상 이벤트와 동일합니다.
2. payment.completed (일반결제 완료)
일반결제 완료 이벤트입니다. 이 문서의 기준 스키마이므로, 다른 이벤트 섹션은 이 payload 대비 차이만 적습니다.
필드 명세
최상위
경로 | 타입 | 의미 |
|---|---|---|
|
| 그로블이 발급한 결제 식별자 |
|
| 판매자가 전달한 참조값. 전달하지 않았거나 서버에서 폐기된 경우 키 자체가 생략됩니다. 상세 제약은 아래 |
buyer
경로 | 타입 | 의미 |
|---|---|---|
|
|
|
|
| 구매자 표시 이름. 회원은 닉네임, 비회원은 입력 이름 기준 |
|
| 구매자 이메일 |
|
| 구매자 전화번호. 상품 유형과 관계없이 항상 포함 |
content
경로 | 타입 | 의미 |
|---|---|---|
|
| 인코딩된 상품 ID |
|
| 구매 시점 상품명 |
|
|
|
|
| 항상 |
|
| 상품 입력 모드 |
options[]
경로 | 타입 | 의미 |
|---|---|---|
|
| 인코딩된 옵션 ID |
|
| 옵션명 스냅샷 |
|
| 옵션 설명. 없으면 생략 |
|
| 구매 수량 |
|
| 할인 전 단가 |
|
| 할인 후 단가 |
|
| 옵션 할인 블록. 없으면 생략 |
|
|
|
|
| 할인 값 |
|
| 타임딜 할인일 때만 포함 |
|
| 타임딜 라벨 |
|
| 추가 옵션 배열. 없으면 |
|
| 인코딩된 추가 옵션 ID |
|
| 추가 옵션명 |
|
| 추가 옵션 수량 |
|
| 추가 옵션 단가 |
|
| 옵션 소계 |
pricing
경로 | 타입 | 의미 |
|---|---|---|
|
| 항상 |
|
| 할인 전 총액 |
|
| 옵션/타임딜 할인 합계. 쿠폰 할인 제외 |
|
| 실제 결제 금액 |
|
| 쿠폰 할인 금액 |
|
| 쿠폰 블록. 없으면 생략 |
|
| 쿠폰 코드 |
|
| 쿠폰 이름 |
|
| 쿠폰 할인 유형 |
|
| 쿠폰 할인 금액 |
paymentMethod
경로 | 타입 | 의미 |
|---|---|---|
|
|
|
|
| 카드사명 |
|
| 마스킹된 카드번호 |
shipping
content.type == GOODS일 때만 포함됩니다.
경로 | 타입 | 의미 |
|---|---|---|
|
| 주소 |
|
| 도로명/지번 주소 |
|
| 상세 주소 |
|
| 배송 요청 사항 |
questionAnswers[]
경로 | 타입 | 의미 |
|---|---|---|
|
| 질문 본문 |
|
| 질문 유형 |
|
| 필수 여부 |
|
| 구매자 응답 |
|
| 표시 순서 |
payment
경로 | 타입 | 의미 |
|---|---|---|
|
| ISO-8601 KST 형식의 구매 시각 |
trackingLink
구매자가 유입 추적링크로 진입한 결제만 포함됩니다. 추적링크 없이 직접 진입한 결제면 trackingLink 블록 자체가 생략됩니다.
경로 | 타입 | 의미 |
|---|---|---|
|
| 유입 추적링크 코드 (예: |
|
| 유입 추적링크 이름 (판매자 설정값) |
sellerReference
판매자가 결제창 진입 링크에 ?ref=<value>를 붙여 값을 지정한 경우에만 포함됩니다. 전달하지 않았거나 아래 제약을 위반해 서버에서 값이 폐기된 경우 키 자체가 생략됩니다.
경로 | 타입 | 의미 |
|---|---|---|
|
| 판매자 시스템의 주문/사용자 식별자 등 불투명 참조값 |
포함되는 이벤트 범위
sellerReference는 결제가 시도된 이벤트와 정기결제 상태 변화 이벤트에 포함됩니다:payment.completed,subscription_payment.completed(최초 결제INITIAL뿐 아니라 이후 모든 갱신RENEWAL회차에도 동일하게 포함),subscription_payment.failed,subscription.cancel_requested,subscription.terminatedpayment.cancel_requested·payment.refunded·subscription_payment.refunded에는 포함되지 않습니다.이 세 이벤트는
merchantUid가 취소·환불되는 그 결제 건과 같은 값이라, 완료 이벤트에서 이미 받아 둔merchantUid로 그대로 매칭하면 됩니다.완료 이벤트를 받은 시점에
merchantUid ↔ sellerReference매핑을 판매자 DB에 저장해 두면 됩니다.다만 정기결제는 회차마다
merchantUid가 새로 발급되므로 정기결제 자체를 가리키는 값이 아닙니다. 정기결제 단위로 연결할 때는 회차와 무관하게 동일한sellerReference를 써 주세요.
값 보장
서버가 값을 수용하면(허용 문자·길이 제약을 모두 통과하면) 저장·반환 과정에서 정규화·대소문자 변환·잘라내기 없이 원문 바이트 그대로 저장되고, 웹훅 payload에도 전송한 값과 동일하게 실립니다.
형식 제약
허용 문자
[A-Za-z0-9-_.:=~], 최대 128자. 사전 검증용 정규식:^[A-Za-z0-9\-_.:=~]{1,128}$제약을 위반하면 값 전체가 폐기되어 payload에서 키가 생략됩니다. 잘라서 저장하지 않으며, 주문 생성이 실패하지도 않습니다.
결제는 정상 진행되고 참조값만 조용히 사라지므로 전송 전 검증은 판매자 쪽 몫입니다
값 설계·인코딩 방법은 결제창 연동 가이드를 참고해 주세요.
멱등성 · 매칭
결제 확정 여부는 반드시 서명 검증을 통과한 웹훅으로만 판단해 주세요(리다이렉트로는 절대 판단하지 말아 주세요). 결제 건 매칭·중복 처리 방지(멱등 키)에는 sellerReference가 아니라 merchantUid를 사용해야 합니다. sellerReference는 판매자 시스템과의 매핑용 보조 값일 뿐, 식별자나 멱등 키로 쓰도록 설계되지 않았습니다 (연동 가이드의 "처리 규칙" 참고)
JSON 예시
펼쳐서 보기 👀
{
"id": "evt_test_a1b2c3d4e5f60718293a4b5c",
"type": "payment.completed",
"version": "2026-04-30",
"occurredAt": "2026-04-20T14:22:11+09:00",
"data": {
"object": {
"merchantUid": "test_merchant_0001",
"buyer": {
"type": "MEMBER",
"displayName": "테스트 구매자",
"email": "test@buyer.example",
"phoneNumber": "01012345678"
},
"content": {
"id": "test_content_0001",
"title": "테스트 상품",
"type": "DOCUMENT",
"paymentType": "ONE_TIME",
"inputMode": "NORMAL"
},
"options": [
{
"optionId": "test_option_0001",
"name": "기본 옵션",
"description": "테스트용 기본 옵션",
"quantity": 1,
"unitOriginalPrice": 10000,
"unitDiscountedPrice": 10000,
"additionalOptions": [],
"subtotal": 10000
}
],
"pricing": {
"currency": "KRW",
"originalAmount": 10000,
"optionDiscountAmount": 0,
"finalAmount": 10000,
"couponDiscountAmount": 0
},
"paymentMethod": {
"type": "CARD",
"cardName": "테스트카드",
"maskedCardNumber": "1234-****-****-5678"
},
"questionAnswers": [],
"payment": {
"purchasedAt": "2026-04-20T14:22:11+09:00"
},
"trackingLink": {
"code": "x5w7s62u",
"name": "인스타그램 광고"
},
"sellerReference": "usr_sample_42"
}
}
}
3. payment.cancel_requested (일반결제 취소 요청)
일반결제 취소 요청 이벤트입니다. 요청 단계이므로 최종 처리 완료 이벤트로 보면 안 됩니다.
payment.completed 대비 차이
경로 | 타입 | 의미 |
|---|---|---|
|
| 원 결제와 동일한 값 |
|
|
|
|
| 포함되지 않음. |
|
| 포함되지 않음 |
|
| 원 구매 시각 |
|
| PG 조회 실패 시 생략 가능 |
|
| PG 조회 실패 시 생략 가능 |
cancelRequest
경로 | 타입 | 의미 |
|---|---|---|
|
| 사유 블록 |
|
|
|
|
| 사유 한글 라벨 |
|
| 상세 사유. 없으면 생략 |
|
| 현재 |
|
| ISO-8601 KST 형식의 취소 요청 시각 |
참고
shipping은content.type == GOODS일 때만 포함됩니다.buyer.phoneNumber는 상품 유형과 관계없이 항상 포함됩니다.즉시 접근 차단이나 최종 정산 반영 기준으로 쓰면 안 됩니다.
JSON 예시
펼쳐서 보기 👀
{
"id": "evt_test_c1d2e3f4a5b60718293c4d5e",
"type": "payment.cancel_requested",
"version": "2026-04-30",
"occurredAt": "2026-04-20T14:22:11+09:00",
"data": {
"object": {
"merchantUid": "test_merchant_0001",
"buyer": {
"type": "MEMBER",
"displayName": "테스트 구매자",
"email": "test@buyer.example",
"phoneNumber": "01012345678"
},
"content": {
"id": "test_content_0001",
"title": "테스트 실물 상품",
"type": "GOODS",
"paymentType": "ONE_TIME",
"inputMode": "NORMAL"
},
"options": [
{
"optionId": "test_option_0001",
"name": "기본 옵션",
"description": "테스트용 기본 옵션",
"quantity": 1,
"unitOriginalPrice": 10000,
"unitDiscountedPrice": 10000,
"additionalOptions": [],
"subtotal": 10000
}
],
"pricing": {
"currency": "KRW",
"originalAmount": 10000,
"optionDiscountAmount": 0,
"finalAmount": 10000,
"couponDiscountAmount": 0
},
"paymentMethod": {
"type": "CARD",
"cardName": "테스트카드",
"maskedCardNumber": "1234-****-****-5678"
},
"shipping": {
"address": "서울시 강남구 테헤란로 123",
"streetAddress": "서울시 강남구 테헤란로 123",
"detailAddress": "101동 202호",
"deliveryRequest": "문 앞에 놓아주세요"
},
"questionAnswers": [],
"cancelRequest": {
"reason": {
"code": "CHANGED_MIND",
"label": "마음이 바뀌었어요"
},
"detailReason": "색상이 마음에 들지 않아요",
"requestedBy": "BUYER",
"requestedAt": "2026-04-20T14:22:11+09:00"
},
"payment": {
"purchasedAt": "2026-04-19T14:22:11+09:00"
}
}
}
}
4. payment.refunded (일반결제 취소 완료)
일반결제 취소 완료 이벤트입니다. 취소가 실제로 처리된 시점에 발행되므로, 접근 차단이나 판매자 시스템 반영은 이 이벤트를 기준으로 하면 됩니다.
payment.cancel_requested는 구매자의 요청 시점이고, 이 이벤트는 처리 완료 시점입니다. 구매자 취소 요청을 판매자가 승인한 건이라면 두 이벤트가 순서대로 도착하지만, 판매자·운영팀이 직접 취소한 건이나 구매자가 즉시 취소한 건은 요청 이벤트 없이 이 이벤트만 도착합니다.
정기결제 회차의 취소는 이 이벤트가 아니라 subscription_payment.refunded로 전달됩니다. payment.completed와 subscription_payment.completed가 나뉘는 것과 같은 기준입니다.
payment.completed 대비 차이
경로 | 타입 | 의미 |
|---|---|---|
|
| 원 결제와 동일한 값 |
|
| 원 구매 시각 |
|
| 원 구매 금액 그대로입니다. 이번에 환불된 금액은 |
|
| 포함되지 않음 |
|
| 포함되지 않음 |
|
| 포함되지 않음 |
|
| 포함되지 않음. |
|
| 추가됨 (아래 참고) |
paymentMethod·questionAnswers[]·sellerReference는 의도적으로 생략합니다. 취소 이벤트는 "얼마가, 왜, 누구에 의해 취소되었는가"만 전달하고, 원 결제의 부가 정보가 필요하면 완료 이벤트를 받은 시점에 저장해 둔 값을merchantUid로 조회해 쓰면 됩니다.
refund
경로 | 타입 | 의미 |
|---|---|---|
|
| 이번 환불 금액. |
|
| 항상 |
|
| 부분 환불 여부 |
|
|
|
|
| 환불 사유. 사유가 남아 있지 않으면 생략 |
|
| ISO-8601 KST 형식의 환불 완료 시각 |
부분 환불 누적 계산
refund.partialRefund == true이면 아래를 전제로 처리해 주세요.
부분 취소 1건마다 이벤트가 1개씩 발행됩니다. 같은 결제(
merchantUid)에 대해 이 이벤트가 여러 번 도착할 수 있습니다.refund.amount는 매번 이번 건의 금액이고, 형제 블록인pricing은 계속 원 구매 금액을 가리킵니다.pricing은 환불이 일어나도 줄어들지 않습니다.따라서 누적 환불액이 필요하면 수신 측에서 이벤트를 합산해야 합니다. 합산 시 중복 집계를 막으려면
merchantUid+refund.refundedAt조합을 멱등 키로 써 주세요.
refund.cancelledBy 읽는 법
값 | 의미 |
|---|---|
| 구매자가 발신한 취소입니다. 구매자의 직접 취소와 "구매자 취소 요청 → 판매자 승인" 두 경우가 모두 포함됩니다 (둘 다 취소를 발신한 쪽은 구매자이기 때문입니다). 승인 건인지 구분해야 하면, 같은 |
| 판매자가 직접 취소한 건 |
| 그로블 운영팀이 취소한 건 |
JSON 예시
펼쳐서 보기 👀
{
"id": "evt_test_e1f2a3b4c5d60718293e4f5a",
"type": "payment.refunded",
"version": "2026-04-30",
"occurredAt": "2026-04-20T14:22:11+09:00",
"data": {
"object": {
"merchantUid": "test_merchant_0001",
"buyer": {
"type": "MEMBER",
"displayName": "테스트 구매자",
"email": "test@buyer.example",
"phoneNumber": "01012345678"
},
"content": {
"id": "test_content_0001",
"title": "테스트 상품",
"type": "DOCUMENT",
"paymentType": "ONE_TIME",
"inputMode": "NORMAL"
},
"options": [
{
"optionId": "test_option_0001",
"name": "기본 옵션",
"description": "테스트용 기본 옵션",
"quantity": 1,
"unitOriginalPrice": 10000,
"unitDiscountedPrice": 10000,
"additionalOptions": [],
"subtotal": 10000
}
],
"pricing": {
"currency": "KRW",
"originalAmount": 10000,
"optionDiscountAmount": 0,
"finalAmount": 10000,
"couponDiscountAmount": 0
},
"payment": {
"purchasedAt": "2026-04-18T14:22:11+09:00"
},
"refund": {
"amount": 10000,
"currency": "KRW",
"partialRefund": false,
"cancelledBy": "BUYER",
"reason": "마음이 바뀌었어요",
"refundedAt": "2026-04-20T14:22:11+09:00"
}
}
}
}
5. subscription_payment.completed (정기결제 완료)
정기결제 완료 이벤트입니다. payment.completed와 거의 같은 구조에 subscription 블록이 추가됩니다.
trackingLink블록(유입 추적링크)은payment.completed와 동일하게 포함됩니다. 최초 결제(INITIAL)뿐 아니라 갱신(RENEWAL) 회차에도 가입 시점의 유입 추적링크가 동일하게 실립니다.
sellerReference도payment.completed와 동일하게 포함됩니다. 주문 생성 시점에 저장된 값이 최초 결제(INITIAL)뿐 아니라 이후 모든 갱신(RENEWAL) 회차 웹훅에도 동일하게 반환됩니다. 제약·권장 사항은sellerReference절을 참고해 주세요.
payment.completed 대비 차이
경로 | 타입 | 의미 |
|---|---|---|
|
| 회원이 결제하면 |
|
| 항상 |
|
| 항상 길이 1 |
|
| 항상 1 |
|
| 항상 |
|
|
|
|
|
|
subscription
경로 | 타입 | 의미 |
|---|---|---|
|
|
|
|
| 현재 결제 회차 |
|
|
|
|
| 항상 |
|
| ISO-8601 KST 형식의 최초 가입 시각 |
|
| ISO-8601 KST 형식의 직전 결제 성공 시각 |
|
| 청구 주기(개월, 1~12). 정기결제 시작 시 동결된 값 |
JSON 예시
펼쳐서 보기 👀
{
"id": "evt_test_b1c2d3e4f5a60718293b4c5d",
"type": "subscription_payment.completed",
"version": "2026-04-30",
"occurredAt": "2026-04-20T14:22:11+09:00",
"data": {
"object": {
"merchantUid": "test_merchant_0001",
"buyer": {
"type": "MEMBER",
"displayName": "테스트 구매자",
"email": "test@buyer.example",
"phoneNumber": "01012345678"
},
"content": {
"id": "test_content_0001",
"title": "테스트 정기결제 상품",
"type": "DOCUMENT",
"paymentType": "SUBSCRIPTION",
"inputMode": "NORMAL"
},
"options": [
{
"optionId": "test_option_0001",
"name": "월간 플랜",
"description": "테스트용 월간 정기결제 옵션",
"quantity": 1,
"unitOriginalPrice": 9900,
"unitDiscountedPrice": 9900,
"additionalOptions": [],
"subtotal": 9900
}
],
"pricing": {
"currency": "KRW",
"originalAmount": 9900,
"optionDiscountAmount": 0,
"finalAmount": 9900,
"couponDiscountAmount": 0
},
"paymentMethod": {
"type": "CARD",
"cardName": "테스트카드",
"maskedCardNumber": "1234-****-****-5678"
},
"questionAnswers": [],
"payment": {
"purchasedAt": "2026-04-20T14:22:11+09:00"
},
"trackingLink": {
"code": "x5w7s62u",
"name": "인스타그램 광고"
},
"sellerReference": "usr_sample_42",
"subscription": {
"billingReason": "INITIAL",
"currentRound": 1,
"nextBillingDate": "2026-05-20",
"status": "ACTIVE",
"activatedAt": "2026-04-20T14:22:11+09:00",
"lastBillingSucceededAt": "2026-04-20T14:22:11+09:00",
"billingCycleMonths": 1
}
}
}
}
6. subscription_payment.refunded (정기결제 취소 완료)
정기결제 취소 완료 이벤트입니다. 정기결제 전체가 아니라 회차 결제 1건에 대한 취소입니다.
payment.refunded와 같은 구조에 subscription 블록이 추가됩니다. 취소된 결제가 정기결제 회차이면 이 이벤트로, 일반결제이면 payment.refunded로 발행됩니다.
회차 취소는 해지가 아닙니다. 이 이벤트를 받아도 정기결제는 대개 그대로 유지됩니다.
subscription.status는 보통ACTIVE(활성)이고nextBillingDate도 바뀌지 않아 다음 회차는 예정대로 청구됩니다. 정기결제를 끊는 것은 별도 이벤트인subscription.terminated로 전달되므로, 이 이벤트를 접근 차단 신호로 쓰지 말아 주세요.
payment.refunded 대비 차이
경로 | 타입 | 의미 |
|---|---|---|
|
| 항상 |
|
| 항상 길이 1 |
|
| 항상 1 |
|
| 항상 |
|
| 추가됨 (아래 참고) |
subscription
경로 | 타입 | 의미 |
|---|---|---|
|
| 환불된 회차. 이번 환불이 몇 회차 결제분인지 |
|
| 정기결제의 현재 결제 회차. 과거 회차를 소급 환불하면 |
|
| 환불 시점의 정기결제 상태. 회차 환불만으로는 해지되지 않으므로 보통 |
|
|
|
|
| ISO-8601 KST 형식의 최초 가입 시각 |
|
| 청구 주기(개월, 1~12). 정기결제 시작 시 동결된 값 |
참고
부분 환불 누적 계산과
refund.cancelledBy해석은 일반결제 취소 완료와 동일합니다.refund.partialRefund == true인 회차 부분 환불에서도pricing은 원 구매 금액을 그대로 유지합니다.
JSON 예시
펼쳐서 보기 👀
{
"id": "evt_test_f1a2b3c4d5e60718293f4a5b",
"type": "subscription_payment.refunded",
"version": "2026-04-30",
"occurredAt": "2026-04-20T14:22:11+09:00",
"data": {
"object": {
"merchantUid": "test_merchant_0001",
"buyer": {
"type": "MEMBER",
"displayName": "테스트 구매자",
"email": "test@buyer.example",
"phoneNumber": "01012345678"
},
"content": {
"id": "test_content_0001",
"title": "테스트 정기결제 상품",
"type": "DOCUMENT",
"paymentType": "SUBSCRIPTION",
"inputMode": "NORMAL"
},
"options": [
{
"optionId": "test_option_0001",
"name": "월간 플랜",
"description": "테스트용 월간 정기결제 옵션",
"quantity": 1,
"unitOriginalPrice": 9900,
"unitDiscountedPrice": 9900,
"additionalOptions": [],
"subtotal": 9900
}
],
"pricing": {
"currency": "KRW",
"originalAmount": 9900,
"optionDiscountAmount": 0,
"finalAmount": 9900,
"couponDiscountAmount": 0
},
"payment": {
"purchasedAt": "2026-04-18T14:22:11+09:00"
},
"refund": {
"amount": 9900,
"currency": "KRW",
"partialRefund": false,
"cancelledBy": "SELLER",
"reason": "판매자 직권 취소",
"refundedAt": "2026-04-20T14:22:11+09:00"
},
"subscription": {
"refundedRound": 3,
"currentRound": 3,
"status": "ACTIVE",
"nextBillingDate": "2026-05-18",
"activatedAt": "2026-01-20T14:22:11+09:00",
"billingCycleMonths": 1
}
}
}
}
7. subscription_payment.failed (정기결제 갱신 실패)
정기결제 갱신 결제가 실패한 이벤트입니다. 매 실패 시도마다 1건씩 발행되므로, 같은 정기결제에 대해 여러 번 도착하는 것이 정상입니다. failure.attemptNumber로 몇 번째 시도인지, failure.isFinal로 마지막 시도였는지 구분해 주세요.
실패 → 재시도 → 종료 흐름
갱신 결제가 실패하면 이 이벤트가
failure.isFinal == false로 발행되고,failure.nextRetryAt에 다음 재시도 예정 시각이 실립니다. 이때subscription.status는PAST_DUE(연체)입니다.재시도가 최대 횟수에 도달하면 마지막 이벤트가
failure.isFinal == true로 발행되고,failure.gracePeriodEndsAt에 유예기간 종료 예정 시각이 실립니다.유예기간이 끝나면
subscription.terminated가termination.reason == PAYMENT_FAILURE_GRACE_EXPIRED로 발행됩니다. 정기결제 해지 완료는 이 이벤트를 기준으로 처리해 주세요.
기본값은 최대 3회 재시도 · 1일 간격 · 유예기간 7일입니다.
subscription_payment.completed 대비 차이
경로 | 타입 | 의미 |
|---|---|---|
|
| 이번 실패 시도의 결제 식별자입니다. 갱신 회차는 시도할 때마다 결제 건이 새로 발급되므로, 재시도 3회는 서로 다른 값 3개로 도착합니다. 결제가 이뤄지지 않았으므로 이 값으로는 완료 이벤트를 받은 적이 없습니다. 정기결제 매칭에 쓰지 마세요 (아래 |
|
| 청구를 시도한 금액 (결제되지 않았습니다) |
|
| 포함되지 않음 |
|
| 포함되지 않음 |
|
| 포함되지 않음 |
|
| 포함되지 않음 |
|
| 최초 결제 때 판매자가 넘긴 참조값이 그대로 실립니다. 갱신 회차마다 동일한 값이므로 이 이벤트를 판매자 시스템의 정기결제에 연결하는 키로 써 주세요. 최초 결제 때 값을 넘기지 않았다면 키가 생략됩니다 |
|
| 항상 |
|
|
|
|
| ISO-8601 KST 형식의 직전 결제 성공 시각. 없으면 생략 |
|
| 추가됨 (아래 참고) |
failure
경로 | 타입 | 의미 |
|---|---|---|
|
| 몇 번째 실패 시도인지 |
|
| 마지막 시도 여부. |
|
| 실패 사유 (예: |
|
| ISO-8601 KST 형식의 실패 시각 |
|
| ISO-8601 KST 형식의 다음 재시도 예정 시각. |
|
| ISO-8601 KST 형식의 유예기간 종료 예정 시각. |
참고
같은 실패 시도가 재전송되어도 중복 처리하지 않으려면
merchantUid+failure.attemptNumber를 멱등 키로 써 주세요.어느 정기결제인지 찾을 때는
sellerReference를 써 주세요.merchantUid는 성공·실패를 막론하고 회차마다 새로 발급되므로 정기결제를 가리키는 값이 아닙니다.이 이벤트는 결제 실패 사실만 알립니다. 정기결제를 종료하는 판단은
subscription.terminated로 해 주세요.
JSON 예시
펼쳐서 보기 👀
{
"id": "evt_test_a2b3c4d5e6f70718293a5b6c",
"type": "subscription_payment.failed",
"version": "2026-04-30",
"occurredAt": "2026-04-20T14:22:11+09:00",
"data": {
"object": {
"merchantUid": "test_merchant_0001",
"sellerReference": "usr_sample_42",
"buyer": {
"type": "MEMBER",
"displayName": "테스트 구매자",
"email": "test@buyer.example",
"phoneNumber": "01012345678"
},
"content": {
"id": "test_content_0001",
"title": "테스트 정기결제 상품",
"type": "DOCUMENT",
"paymentType": "SUBSCRIPTION",
"inputMode": "NORMAL"
},
"options": [
{
"optionId": "test_option_0001",
"name": "월간 플랜",
"description": "테스트용 월간 정기결제 옵션",
"quantity": 1,
"unitOriginalPrice": 9900,
"unitDiscountedPrice": 9900,
"additionalOptions": [],
"subtotal": 9900
}
],
"pricing": {
"currency": "KRW",
"originalAmount": 9900,
"optionDiscountAmount": 0,
"finalAmount": 9900,
"couponDiscountAmount": 0
},
"subscription": {
"billingReason": "RENEWAL",
"currentRound": 3,
"nextBillingDate": "2026-04-23",
"status": "PAST_DUE",
"activatedAt": "2026-02-20T14:22:11+09:00",
"lastBillingSucceededAt": "2026-03-20T14:22:11+09:00",
"billingCycleMonths": 1
},
"failure": {
"attemptNumber": 1,
"isFinal": false,
"reason": "CARD_EXPIRED",
"failedAt": "2026-04-20T14:22:11+09:00",
"nextRetryAt": "2026-04-21T14:22:11+09:00"
}
}
}
}
8. subscription.cancel_requested (정기결제 해지 요청)
정기결제 해지 요청 이벤트입니다. 구매자가 해지를 신청한 시점의 정기결제 상태 스냅샷을 전달합니다. 요청 즉시 정기결제가 끊기지는 않습니다. 이미 결제된 이용기간이 끝나면 subscription.terminated가 따로 발행됩니다.
subscription_payment.completed 대비 차이
경로 | 타입 | 의미 |
|---|---|---|
|
| 해지 요청 시점까지 가장 최근에 결제된 회차의 결제 식별자입니다. 갱신 회차는 결제마다 식별자가 새로 발급되므로 최초(1회차) 결제와 다를 수 있습니다. 정기결제 매칭은 |
|
| 포함되지 않음 |
|
| 포함됩니다. 최초 결제 시 입력한 질문·응답 스냅샷. 없으면 |
|
| 포함되지 않음 |
|
| 최초 결제 때 판매자가 넘긴 참조값이 그대로 실립니다. 갱신 회차마다 동일한 값이므로 이 이벤트를 판매자 시스템의 정기결제에 연결하는 키로 써 주세요. 최초 결제 때 값을 넘기지 않았다면 키가 생략됩니다 |
|
| 원 구매 시각 |
subscription
경로 | 타입 | 의미 |
|---|---|---|
|
| 항상 |
|
| 해지 시점의 누적 결제 회차 |
|
|
|
|
| ISO-8601 KST 형식의 최초 가입 시각 |
|
| ISO-8601 KST 형식의 해지 요청 시각 |
|
| 직전 결제 금액 |
|
| 항상 |
|
| 직전 실패 사유. 없으면 생략 |
|
| 청구 주기(개월, 1~12). 정기결제 시작 시 동결된 값 |
cancelRequest
경로 | 타입 | 의미 |
|---|---|---|
|
| 사유 블록 |
|
|
|
|
| 사유 한글 라벨 |
|
| 상세 사유. 값이 있으면 포함 |
|
| 현재 |
|
| ISO-8601 KST 형식의 해지 요청 시각 |
참고
options[]는 이 이벤트에서도 배열 형태로 전달됩니다.현재 자발적 해지 payload에서는
gracePeriodEndsAt이 포함되지 않습니다.gracePeriodEndsAt은 결제 재시도 실패 후 유예기간 흐름에서 사용하는 값입니다.
JSON 예시
펼쳐서 보기 👀
{
"id": "evt_test_d1e2f3a4b5c60718293d4e5f",
"type": "subscription.cancel_requested",
"version": "2026-04-30",
"occurredAt": "2026-04-20T14:22:11+09:00",
"data": {
"object": {
"merchantUid": "test_merchant_0001",
"sellerReference": "usr_sample_42",
"buyer": {
"type": "MEMBER",
"displayName": "테스트 구매자",
"email": "test@buyer.example",
"phoneNumber": "01012345678"
},
"content": {
"id": "test_content_0001",
"title": "테스트 정기결제 상품",
"type": "DOCUMENT",
"paymentType": "SUBSCRIPTION",
"inputMode": "NORMAL"
},
"options": [
{
"optionId": "test_option_0001",
"name": "월간 플랜",
"description": "테스트용 월간 정기결제 옵션",
"quantity": 1,
"unitOriginalPrice": 9900,
"unitDiscountedPrice": 9900,
"additionalOptions": [],
"subtotal": 9900
}
],
"pricing": {
"currency": "KRW",
"originalAmount": 9900,
"optionDiscountAmount": 0,
"finalAmount": 9900,
"couponDiscountAmount": 0
},
"questionAnswers": [],
"payment": {
"purchasedAt": "2026-01-20T14:22:11+09:00"
},
"subscription": {
"status": "CANCELLED",
"currentRound": 3,
"nextBillingDate": "2026-05-05",
"activatedAt": "2026-01-20T14:22:11+09:00",
"cancelledAt": "2026-04-20T14:22:11+09:00",
"price": 9900,
"currency": "KRW",
"billingCycleMonths": 1
},
"cancelRequest": {
"reason": {
"code": "ETC",
"label": "기타"
},
"detailReason": "지금은 더 이상 사용하지 않아요",
"requestedBy": "BUYER",
"requestedAt": "2026-04-20T14:22:11+09:00"
}
}
}
}
9. subscription.terminated (정기결제 해지 완료)
정기결제 해지 완료 이벤트입니다. 정기결제가 실제로 끝나는 시점에 발행되므로, 상품 접근 차단은 이 이벤트를 기준으로 처리하면 됩니다.
subscription.cancel_requested와 시점이 다릅니다. 해지 요청은 "다음 회차부터 갱신하지 않겠다"는 예고이고, 그 시점에는 이미 결제된 이용기간이 남아 있어 정기결제가 유지됩니다. 이 이벤트는 그 이용기간까지 끝난 시점에 도착합니다.
정기결제가 끝나는 경로는 여러 개지만 이벤트는 이것 하나입니다. 어떤 경로로 끝났는지는 termination.reason으로 구분해 주세요. "해지 완료"를 알기 위해 여러 이벤트를 구독하고 OR로 묶지 않아도 됩니다.
subscription.cancel_requested 대비 차이
경로 | 타입 | 의미 |
|---|---|---|
|
| 포함되지 않음 |
|
| 항상 |
|
| ISO-8601 KST 형식의 해지 처리 시각. 정기결제가 완전히 종료된 시각은 |
|
| 직전 갱신 결제 실패 사유. |
|
| 포함되지 않음 |
|
| 포함되지 않음 |
|
| 포함되지 않음. 해지 요청 사유는 선행 |
|
| 추가됨 (아래 참고) |
termination
경로 | 타입 | 의미 |
|---|---|---|
|
| 해지 경로. |
|
| ISO-8601 KST 형식의 정기결제가 실제로 끝난 시각. 기간 만료형은 이용기간 만료 시점, 즉시 종료형은 처리 시각 |
|
| ISO-8601 KST 형식의 유예기간 만료 시각. |
termination.reason 읽는 법
값 | 의미 |
|---|---|
| 해지 이후 결제된 이용기간이 만료되었습니다. 가장 흔한 경로입니다. 구매자 자진 해지와 운영팀 해지를 모두 포함합니다. 두 경로 모두 즉시 정기결제가 종료되지 않고 이미 결제된 기간까지 이용을 보장한 뒤 종료되기 때문입니다. 구매자가 직접 해지한 건인지 구분해야 하면, 선행 |
| 갱신 결제가 반복 실패해 유예기간까지 소진되었습니다. 이 경로에서만 |
| 판매자가 상품을 삭제해 해당 상품의 정기결제가 일괄 종료되었습니다. 잔여 기간과 무관하게 즉시 종료됩니다 |
참고
중복 처리를 막으려면
merchantUid+termination.terminatedAt을 멱등 키로 써 주세요. 해지 후 재가입·재해지가 가능하므로 정기결제 식별자만으로는 구분되지 않습니다.
JSON 예시
가장 흔한 경로인 PERIOD_END 기준 예시입니다. 결제 실패로 인한 종료는 termination.reason이 PAYMENT_FAILURE_GRACE_EXPIRED로 바뀌고 termination.gracePeriodExpiredAt이 추가됩니다.
펼쳐서 보기 👀
{
"id": "evt_test_b2c3d4e5f6a70718293b5c6d",
"type": "subscription.terminated",
"version": "2026-04-30",
"occurredAt": "2026-04-20T14:22:11+09:00",
"data": {
"object": {
"merchantUid": "test_merchant_0001",
"sellerReference": "usr_sample_42",
"buyer": {
"type": "MEMBER",
"displayName": "테스트 구매자",
"email": "test@buyer.example",
"phoneNumber": "01012345678"
},
"content": {
"id": "test_content_0001",
"title": "테스트 정기결제 상품",
"type": "DOCUMENT",
"paymentType": "SUBSCRIPTION",
"inputMode": "NORMAL"
},
"options": [
{
"optionId": "test_option_0001",
"name": "월간 플랜",
"description": "테스트용 월간 정기결제 옵션",
"quantity": 1,
"unitOriginalPrice": 9900,
"unitDiscountedPrice": 9900,
"additionalOptions": [],
"subtotal": 9900
}
],
"pricing": {
"currency": "KRW",
"originalAmount": 9900,
"optionDiscountAmount": 0,
"finalAmount": 9900,
"couponDiscountAmount": 0
},
"subscription": {
"status": "CANCELLED",
"currentRound": 3,
"activatedAt": "2026-01-20T14:22:11+09:00",
"cancelledAt": "2026-04-05T14:22:11+09:00",
"price": 9900,
"currency": "KRW",
"billingCycleMonths": 1
},
"termination": {
"reason": "PERIOD_END",
"terminatedAt": "2026-04-20T14:22:11+09:00"
}
}
}
}
업데이트 이력
날짜 | 업데이트 내용 |
|---|---|
|
|
| 모든 이벤트 |
| 변경: |
| 변경: |
| 공개 이벤트 4종 → 8종으로 확대. |
| 변경: |