가이드

웹훅 이벤트

1. 이벤트 한눈에 보기

이벤트 타입

의미

payment.completed

일반결제 완료

payment.cancel_requested

일반결제 취소 요청

payment.refunded

일반결제 취소 완료

subscription_payment.completed

정기결제 완료

subscription_payment.refunded

정기결제 취소 완료 (회차 단위)

subscription_payment.failed

정기결제 갱신 실패

subscription.cancel_requested

정기결제 해지 요청

subscription.terminated

정기결제 해지 완료

이름 규칙

  • 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 대비 차이만 적습니다.

필드 명세

최상위

경로

타입

의미

merchantUid

string

그로블이 발급한 결제 식별자

sellerReference

string

판매자가 전달한 참조값. 전달하지 않았거나 서버에서 폐기된 경우 키 자체가 생략됩니다. 상세 제약은 아래 sellerReference 절 참고

buyer

경로

타입

의미

buyer.type

string

MEMBER(회원) 또는 GUEST(비회원)

buyer.displayName

string

구매자 표시 이름. 회원은 닉네임, 비회원은 입력 이름 기준

buyer.email

string

구매자 이메일

buyer.phoneNumber

string

구매자 전화번호. 상품 유형과 관계없이 항상 포함

content

경로

타입

의미

content.id

string

인코딩된 상품 ID

content.title

string

구매 시점 상품명

content.type

string

DOCUMENT(자료) · SERVICE(서비스) · GOODS(제품) · EVENT(이벤트)

content.paymentType

string

항상 ONE_TIME(일반결제)

content.inputMode

string

상품 입력 모드
NORMAL(판매 페이지) · SIMPLE(간편 모드) · PAYMENT_WINDOW(결제창)
간편 모드는 2026년 07월 01일부터 신규 생성이 불가합니다.

options[]

경로

타입

의미

options[].optionId

string

인코딩된 옵션 ID

options[].name

string

옵션명 스냅샷

options[].description

string

옵션 설명. 없으면 생략

options[].quantity

number

구매 수량

options[].unitOriginalPrice

number

할인 전 단가

options[].unitDiscountedPrice

number

할인 후 단가

options[].discount

object

옵션 할인 블록. 없으면 생략

options[].discount.type

string

AMOUNT(원화 할인) 또는 PERCENT(% 할인)

options[].discount.value

number

할인 값

options[].discount.timeDeal

object

타임딜 할인일 때만 포함

options[].discount.timeDeal.label

string

타임딜 라벨

options[].additionalOptions

array

추가 옵션 배열. 없으면 []

options[].additionalOptions[].id

string

인코딩된 추가 옵션 ID

options[].additionalOptions[].name

string

추가 옵션명

options[].additionalOptions[].quantity

number

추가 옵션 수량

options[].additionalOptions[].unitPrice

number

추가 옵션 단가

options[].subtotal

number

옵션 소계

pricing

경로

타입

의미

pricing.currency

string

항상 KRW

pricing.originalAmount

number

할인 전 총액

pricing.optionDiscountAmount

number

옵션/타임딜 할인 합계. 쿠폰 할인 제외

pricing.finalAmount

number

실제 결제 금액

pricing.couponDiscountAmount

number

쿠폰 할인 금액

pricing.coupon

object

쿠폰 블록. 없으면 생략

pricing.coupon.code

string

쿠폰 코드

pricing.coupon.name

string

쿠폰 이름

pricing.coupon.type

string

쿠폰 할인 유형

pricing.coupon.discountAmount

number

쿠폰 할인 금액

paymentMethod

경로

타입

의미

paymentMethod.type

string

CARD(카드 결제) 또는 FREE(무료 결제). FREEcardName·maskedCardNumber가 함께 생략됩니다

paymentMethod.cardName

string

카드사명

paymentMethod.maskedCardNumber

string

마스킹된 카드번호

shipping

content.type == GOODS일 때만 포함됩니다.

경로

타입

의미

shipping.address

string

주소

shipping.streetAddress

string

도로명/지번 주소

shipping.detailAddress

string

상세 주소

shipping.deliveryRequest

string

배송 요청 사항

questionAnswers[]

경로

타입

의미

questionAnswers[].question

string

질문 본문

questionAnswers[].questionType

string

질문 유형

questionAnswers[].required

boolean

필수 여부

questionAnswers[].answer

string

구매자 응답

questionAnswers[].displayOrder

number

표시 순서

payment

경로

타입

의미

payment.purchasedAt

string

ISO-8601 KST 형식의 구매 시각

trackingLink

구매자가 유입 추적링크로 진입한 결제만 포함됩니다. 추적링크 없이 직접 진입한 결제면 trackingLink 블록 자체가 생략됩니다.

경로

타입

의미

trackingLink.code

string

유입 추적링크 코드 (예: x5w7s62u)

trackingLink.name

string

유입 추적링크 이름 (판매자 설정값)

sellerReference

판매자가 결제창 진입 링크에 ?ref=<value>를 붙여 값을 지정한 경우에만 포함됩니다. 전달하지 않았거나 아래 제약을 위반해 서버에서 값이 폐기된 경우 키 자체가 생략됩니다.

경로

타입

의미

sellerReference

string

판매자 시스템의 주문/사용자 식별자 등 불투명 참조값

포함되는 이벤트 범위

  • sellerReference결제가 시도된 이벤트와 정기결제 상태 변화 이벤트에 포함됩니다: payment.completed, subscription_payment.completed(최초 결제 INITIAL뿐 아니라 이후 모든 갱신 RENEWAL 회차에도 동일하게 포함), subscription_payment.failed, subscription.cancel_requested, subscription.terminated

  • payment.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 대비 차이

경로

타입

의미

merchantUid

string

원 결제와 동일한 값

content.type

string

DOCUMENT · SERVICE · GOODS · EVENT 중 하나

sellerReference

string

포함되지 않음. sellerReference는 sellerReference 절의 포함 범위에 해당하는 이벤트에만 실립니다. 취소 건 매칭은 원 결제와 동일한 merchantUid로 해 주세요

pricing.coupon

object

포함되지 않음

payment.purchasedAt

string

원 구매 시각

paymentMethod.cardName

string

PG 조회 실패 시 생략 가능

paymentMethod.maskedCardNumber

string

PG 조회 실패 시 생략 가능

cancelRequest

경로

타입

의미

cancelRequest.reason

object

사유 블록

cancelRequest.reason.code

string

OTHER_PAYMENT_METHOD(다른 수단으로 결제할게요) · CHANGED_MIND(마음이 바뀌었어요) · FOUND_CHEAPER_CONTENT(더 저렴한 콘텐츠를 찾았어요) · ETC(기타)

cancelRequest.reason.label

string

사유 한글 라벨

cancelRequest.detailReason

string

상세 사유. 없으면 생략

cancelRequest.requestedBy

string

현재 BUYER(구매자) 고정

cancelRequest.requestedAt

string

ISO-8601 KST 형식의 취소 요청 시각

참고

  • shippingcontent.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.completedsubscription_payment.completed가 나뉘는 것과 같은 기준입니다.

payment.completed 대비 차이

경로

타입

의미

merchantUid

string

원 결제와 동일한 값

payment.purchasedAt

string

원 구매 시각

pricing

object

원 구매 금액 그대로입니다. 이번에 환불된 금액은 refund.amount에서 확인해 주세요

paymentMethod

object

포함되지 않음

questionAnswers[]

array

포함되지 않음

trackingLink

object

포함되지 않음

sellerReference

string

포함되지 않음. sellerReference는 sellerReference 절의 포함 범위에 해당하는 이벤트에만 실립니다. 취소 건 매칭은 원 결제와 동일한 merchantUid로 해 주세요

refund

object

추가됨 (아래 참고)

paymentMethod · questionAnswers[] · sellerReference는 의도적으로 생략합니다. 취소 이벤트는 "얼마가, 왜, 누구에 의해 취소되었는가"만 전달하고, 원 결제의 부가 정보가 필요하면 완료 이벤트를 받은 시점에 저장해 둔 값을 merchantUid로 조회해 쓰면 됩니다.

refund

경로

타입

의미

refund.amount

number

이번 환불 금액. partialRefund == true면 전액이 아니라 이번 부분 취소분입니다

refund.currency

string

항상 KRW

refund.partialRefund

boolean

부분 환불 여부

refund.cancelledBy

string

BUYER(구매자) · SELLER(판매자) · ADMIN(그로블 운영팀)

refund.reason

string

환불 사유. 사유가 남아 있지 않으면 생략

refund.refundedAt

string

ISO-8601 KST 형식의 환불 완료 시각

부분 환불 누적 계산

refund.partialRefund == true이면 아래를 전제로 처리해 주세요.

  • 부분 취소 1건마다 이벤트가 1개씩 발행됩니다. 같은 결제(merchantUid)에 대해 이 이벤트가 여러 번 도착할 수 있습니다.

  • refund.amount는 매번 이번 건의 금액이고, 형제 블록인 pricing은 계속 원 구매 금액을 가리킵니다. pricing은 환불이 일어나도 줄어들지 않습니다.

  • 따라서 누적 환불액이 필요하면 수신 측에서 이벤트를 합산해야 합니다. 합산 시 중복 집계를 막으려면 merchantUid + refund.refundedAt 조합을 멱등 키로 써 주세요.

refund.cancelledBy 읽는 법

의미

BUYER

구매자가 발신한 취소입니다. 구매자의 직접 취소와 "구매자 취소 요청 → 판매자 승인" 두 경우가 모두 포함됩니다 (둘 다 취소를 발신한 쪽은 구매자이기 때문입니다). 승인 건인지 구분해야 하면, 같은 merchantUid로 선행 payment.cancel_requested를 받은 적이 있는지로 판단해 주세요

SELLER

판매자가 직접 취소한 건

ADMIN

그로블 운영팀이 취소한 건

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) 회차에도 가입 시점의 유입 추적링크가 동일하게 실립니다.

sellerReferencepayment.completed와 동일하게 포함됩니다. 주문 생성 시점에 저장된 값이 최초 결제(INITIAL)뿐 아니라 이후 모든 갱신(RENEWAL) 회차 웹훅에도 동일하게 반환됩니다. 제약·권장 사항은 sellerReference 절을 참고해 주세요.

payment.completed 대비 차이

경로

타입

의미

buyer.type

string

회원이 결제하면 MEMBER(회원), 비회원이 결제하면 GUEST(비회원)

content.paymentType

string

항상 SUBSCRIPTION(정기결제)

options[]

array

항상 길이 1

options[].quantity

number

항상 1

options[].additionalOptions

array

항상 []

paymentMethod

object

billingReason == INITIAL일 때만 포함

questionAnswers[]

array

billingReason == INITIAL일 때만 포함

subscription

경로

타입

의미

subscription.billingReason

string

INITIAL(최초 결제) 또는 RENEWAL(정기 갱신)

subscription.currentRound

number

현재 결제 회차

subscription.nextBillingDate

string

YYYY-MM-DD 형식의 다음 결제 예정일

subscription.status

string

항상 ACTIVE(활성)

subscription.activatedAt

string

ISO-8601 KST 형식의 최초 가입 시각

subscription.lastBillingSucceededAt

string

ISO-8601 KST 형식의 직전 결제 성공 시각

subscription.billingCycleMonths

number

청구 주기(개월, 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 대비 차이

경로

타입

의미

content.paymentType

string

항상 SUBSCRIPTION(정기결제)

options[]

array

항상 길이 1

options[].quantity

number

항상 1

options[].additionalOptions

array

항상 []

subscription

object

추가됨 (아래 참고)

subscription

경로

타입

의미

subscription.refundedRound

number

환불된 회차. 이번 환불이 몇 회차 결제분인지

subscription.currentRound

number

정기결제의 현재 결제 회차. 과거 회차를 소급 환불하면 refundedRound보다 클 수 있습니다

subscription.status

string

환불 시점의 정기결제 상태. 회차 환불만으로는 해지되지 않으므로 보통 ACTIVE(활성)

subscription.nextBillingDate

string

YYYY-MM-DD 형식의 다음 결제 예정일. 회차 환불로 변경되지 않습니다. 없으면 생략

subscription.activatedAt

string

ISO-8601 KST 형식의 최초 가입 시각

subscription.billingCycleMonths

number

청구 주기(개월, 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로 마지막 시도였는지 구분해 주세요.

실패 → 재시도 → 종료 흐름

  1. 갱신 결제가 실패하면 이 이벤트가 failure.isFinal == false로 발행되고, failure.nextRetryAt에 다음 재시도 예정 시각이 실립니다. 이때 subscription.statusPAST_DUE(연체)입니다.

  2. 재시도가 최대 횟수에 도달하면 마지막 이벤트가 failure.isFinal == true로 발행되고, failure.gracePeriodEndsAt에 유예기간 종료 예정 시각이 실립니다.

  3. 유예기간이 끝나면 subscription.terminatedtermination.reason == PAYMENT_FAILURE_GRACE_EXPIRED로 발행됩니다. 정기결제 해지 완료는 이 이벤트를 기준으로 처리해 주세요.

기본값은 최대 3회 재시도 · 1일 간격 · 유예기간 7일입니다.

subscription_payment.completed 대비 차이

경로

타입

의미

merchantUid

string

이번 실패 시도의 결제 식별자입니다. 갱신 회차는 시도할 때마다 결제 건이 새로 발급되므로, 재시도 3회는 서로 다른 값 3개로 도착합니다. 결제가 이뤄지지 않았으므로 이 값으로는 완료 이벤트를 받은 적이 없습니다. 정기결제 매칭에 쓰지 마세요 (아래 sellerReference 참고)

pricing.finalAmount

number

청구를 시도한 금액 (결제되지 않았습니다)

paymentMethod

object

포함되지 않음

questionAnswers[]

array

포함되지 않음

payment

object

포함되지 않음

trackingLink

object

포함되지 않음

sellerReference

string

최초 결제 때 판매자가 넘긴 참조값이 그대로 실립니다. 갱신 회차마다 동일한 값이므로 이 이벤트를 판매자 시스템의 정기결제에 연결하는 키로 써 주세요. 최초 결제 때 값을 넘기지 않았다면 키가 생략됩니다

subscription.billingReason

string

항상 RENEWAL(정기 갱신)

subscription.status

string

PAST_DUE(연체, 재시도 중) 또는 CANCELLED(최종 실패)

subscription.lastBillingSucceededAt

string

ISO-8601 KST 형식의 직전 결제 성공 시각. 없으면 생략

failure

object

추가됨 (아래 참고)

failure

경로

타입

의미

failure.attemptNumber

number

몇 번째 실패 시도인지

failure.isFinal

boolean

마지막 시도 여부. true면 더 이상 재시도하지 않고 유예기간으로 넘어갑니다

failure.reason

string

실패 사유 (예: CARD_EXPIRED)

failure.failedAt

string

ISO-8601 KST 형식의 실패 시각

failure.nextRetryAt

string

ISO-8601 KST 형식의 다음 재시도 예정 시각. isFinal == false일 때만 포함

failure.gracePeriodEndsAt

string

ISO-8601 KST 형식의 유예기간 종료 예정 시각. isFinal == true일 때만 포함

참고

  • 같은 실패 시도가 재전송되어도 중복 처리하지 않으려면 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 대비 차이

경로

타입

의미

merchantUid

string

해지 요청 시점까지 가장 최근에 결제된 회차의 결제 식별자입니다. 갱신 회차는 결제마다 식별자가 새로 발급되므로 최초(1회차) 결제와 다를 수 있습니다. 정기결제 매칭은 sellerReference로 해 주세요

paymentMethod

object

포함되지 않음

questionAnswers[]

array

포함됩니다. 최초 결제 시 입력한 질문·응답 스냅샷. 없으면 []

shipping

object

포함되지 않음

sellerReference

string

최초 결제 때 판매자가 넘긴 참조값이 그대로 실립니다. 갱신 회차마다 동일한 값이므로 이 이벤트를 판매자 시스템의 정기결제에 연결하는 키로 써 주세요. 최초 결제 때 값을 넘기지 않았다면 키가 생략됩니다

payment.purchasedAt

string

원 구매 시각

subscription

경로

타입

의미

subscription.status

string

항상 CANCELLED(해지 요청됨).

subscription.currentRound

number

해지 시점의 누적 결제 회차

subscription.nextBillingDate

string

YYYY-MM-DD 형식. 해지 요청 직전에 예정돼 있던 다음 결제 예정일이 그대로 담깁니다. 이미 결제된 이용기간이 이 날짜까지 이어지므로, 정기결제 해지 완료일의 참고값으로 쓸 수 있습니다

subscription.activatedAt

string

ISO-8601 KST 형식의 최초 가입 시각

subscription.cancelledAt

string

ISO-8601 KST 형식의 해지 요청 시각

subscription.price

number

직전 결제 금액

subscription.currency

string

항상 KRW

subscription.lastBillingFailureReason

string

직전 실패 사유. 없으면 생략

subscription.billingCycleMonths

number

청구 주기(개월, 1~12). 정기결제 시작 시 동결된 값

cancelRequest

경로

타입

의미

cancelRequest.reason

object

사유 블록

cancelRequest.reason.code

string

OTHER_PAYMENT_METHOD(다른 수단으로 결제할게요) · CHANGED_MIND(마음이 바뀌었어요) · FOUND_CHEAPER_CONTENT(더 저렴한 콘텐츠를 찾았어요) · ETC(기타)

cancelRequest.reason.label

string

사유 한글 라벨

cancelRequest.detailReason

string

상세 사유. 값이 있으면 포함

cancelRequest.requestedBy

string

현재 BUYER(구매자) 고정

cancelRequest.requestedAt

string

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 대비 차이

경로

타입

의미

subscription.nextBillingDate

string

포함되지 않음

subscription.status

string

항상 CANCELLED(해지됨)

subscription.cancelledAt

string

ISO-8601 KST 형식의 해지 처리 시각. 정기결제가 완전히 종료된 시각은 termination.terminatedAt입니다

subscription.lastBillingFailureReason

string

직전 갱신 결제 실패 사유. termination.reason == PAYMENT_FAILURE_GRACE_EXPIRED일 때 채워집니다. 없으면 생략

questionAnswers[]

array

포함되지 않음

payment

object

포함되지 않음

cancelRequest

object

포함되지 않음. 해지 요청 사유는 선행 subscription.cancel_requested에서 확인해 주세요

termination

object

추가됨 (아래 참고)

termination

경로

타입

의미

termination.reason

string

해지 경로. PERIOD_END · PAYMENT_FAILURE_GRACE_EXPIRED · CONTENT_DELETED (아래 표 참고). 앞으로 경로가 추가될 수 있으므로, 모르는 값이 오면 무시하고 terminatedAt만으로 해지 완료를 처리해 주세요

termination.terminatedAt

string

ISO-8601 KST 형식의 정기결제가 실제로 끝난 시각. 기간 만료형은 이용기간 만료 시점, 즉시 종료형은 처리 시각

termination.gracePeriodExpiredAt

string

ISO-8601 KST 형식의 유예기간 만료 시각. termination.reason == PAYMENT_FAILURE_GRACE_EXPIRED일 때만 포함

termination.reason 읽는 법

의미

PERIOD_END

해지 이후 결제된 이용기간이 만료되었습니다. 가장 흔한 경로입니다. 구매자 자진 해지와 운영팀 해지를 모두 포함합니다. 두 경로 모두 즉시 정기결제가 종료되지 않고 이미 결제된 기간까지 이용을 보장한 뒤 종료되기 때문입니다. 구매자가 직접 해지한 건인지 구분해야 하면, 선행 subscription.cancel_requested를 받은 적이 있는지로 판단해 주세요 (운영팀 해지는 그 이벤트를 발행하지 않습니다)

PAYMENT_FAILURE_GRACE_EXPIRED

갱신 결제가 반복 실패해 유예기간까지 소진되었습니다. 이 경로에서만 termination.gracePeriodExpiredAt이 함께 실리고, subscription.lastBillingFailureReason도 채워집니다. 선행 흐름은 정기결제 갱신 실패를 참고해 주세요

CONTENT_DELETED

판매자가 상품을 삭제해 해당 상품의 정기결제가 일괄 종료되었습니다. 잔여 기간과 무관하게 즉시 종료됩니다

참고

  • 중복 처리를 막으려면 merchantUid + termination.terminatedAt을 멱등 키로 써 주세요. 해지 후 재가입·재해지가 가능하므로 정기결제 식별자만으로는 구분되지 않습니다.

JSON 예시

가장 흔한 경로인 PERIOD_END 기준 예시입니다. 결제 실패로 인한 종료는 termination.reasonPAYMENT_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"
      }
    }
  }
}

업데이트 이력

날짜

업데이트 내용

2026-06-27

payment.completed · subscription_payment.completed 이벤트에 trackingLink(유입 추적링크 code · name) 필드 추가

2026-07-01

모든 이벤트 content 블록에 상품 입력 모드 content.inputMode(NORMAL · SIMPLE · PAYMENT_WINDOW) 추가. 정기결제 이벤트(subscription_payment.completed · subscription.cancel_requested) subscription 블록에 청구 주기 subscription.billingCycleMonths(1~12개월) 추가

2026-07-02

변경: buyer.phoneNumber(구매자 전화번호)를 모든 웹훅 응답 필드에 추가
기존: contentType==GOODS에만 한정

2026-07-05

변경: questionAnswers[]subscription.cancel_requested(정기결제 해지 요청)에 신규 추가
기존: questionAnswers[]subscription.cancel_requested(정기결제 해지 요청)에서 제외

2026-07-24

공개 이벤트 4종 → 8종으로 확대.
신규: payment.refunded(일반결제 취소 완료) · subscription_payment.refunded(정기결제 취소 완료, 회차 단위) · subscription_payment.failed(정기결제 갱신 실패) · subscription.terminated(정기결제 해지 완료), payment.completed · subscription_payment.completedsellerReference(판매자 참조값)를 추가

2026-07-26

변경: sellerReference(판매자 참조값)를 subscription_payment.failed응답에 추가하여 결제가 시도된 모든 이벤트에 대해 참조값을 활용할 수 있도록 확장
변경: subscription_payment.failedmerchantUid가 직전 성공 회차가 아니라 해당 실패 시도의 결제 식별자로 정정
변경: sellerReference(판매자 참조값)를 subscription.cancel_requested · subscription.terminated에 추가. 정기결제 단위 매칭 키를 sellerReference로 일원화
변경: 정기결제 해지 계열 이벤트의 merchantUid가 원 결제가 아니라 가장 최근에 결제된 회차의 식별자임을 명시