{
  "envelope": {
    "description": "모든 웹훅은 같은 봉투를 사용합니다. `HMAC-SHA256(webhook_secret, timestamp\\nraw_body)`를 소문자 hex로 `X-TKE-Webhook-Signature`에 서명합니다. 24시간 교체 구간에는 `X-TKE-Webhook-Signature-Old`도 보냅니다. 최소 1회 전송이므로 `event_id`로 중복 제거하세요.",
    "fields": {
      "data": "이벤트 유형별 데이터입니다(아래 참고).",
      "event_id": "이벤트별로 고유하고 고정된 UUID입니다(중복 제거).",
      "event_type": "이벤트 유형입니다.",
      "is_testmode": "테스트 키에서 발생했으면 true입니다。",
      "occurred_at": "ISO-8601 UTC."
    }
  },
  "health_check": {
    "data_fields": [
      "message"
    ],
    "event_type": "ping",
    "note": "`events`로 등록하거나 해제할 수 없습니다(`ping` 지정 시 `internal.validation_failed`). 모든 엔드포인트가 받습니다. 실제 봉투와 서명(`X-TKE-Webhook-Signature`, `-Old` 없음)을 사용하므로 검증 테스트에 활용하세요. 알 수 없는 `event_type`에도 2xx를 반환해야 합니다. 4xx면 생성·수정이 `webhook.health_check_failed`(422)로 실패하고 저장되지 않습니다. `ping`은 `GET /v1/webhooks/deliveries`에 없습니다.",
    "when": "모든 `POST /v1/webhooks` 및 `PUT /v1/webhooks/{id}`에서 저장 전에 상태 확인"
  },
  "items": [
    {
      "data_fields": [
        "order_id",
        "external_ref",
        "items[] (order_item_id, iccid)"
      ],
      "event_type": "order.completed",
      "note": "",
      "when": "주문 확정 및 eSIM 준비 완료"
    },
    {
      "data_fields": [
        "order_id",
        "external_ref",
        "reason"
      ],
      "event_type": "order.failed",
      "note": "",
      "when": "주문 미완료(발급 실패 등)"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "iccid",
        "lpa"
      ],
      "event_type": "esim.delivered",
      "note": "`order_id`와 `external_ref`로 연결합니다. `lpa`는 POST 직전에 복호화하며 전송 기록에 평문으로 저장하지 않습니다. QR 이미지 URL은 없으므로 `lpa`로 생성하거나 `GET /v1/esims/{id}`에서 `qr_code_base64`를 조회하세요.",
      "when": "eSIM이 발급되고 설치 데이터가 준비됨"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "iccid",
        "activated_at"
      ],
      "event_type": "esim.activated",
      "note": "`order_id`와 구매 시 보낸 `external_ref`로 eSIM을 주문과 연결하세요。",
      "when": "eSIM 설치 및 활성화(smdp enabled/in_use)"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "threshold (75/90)",
        "used_bytes",
        "data_bytes"
      ],
      "event_type": "esim.usage_threshold",
      "note": "`order_id`와 `external_ref`로 주문에 연결합니다. 새로 넘은 기준 중 가장 높은 값만 발송합니다(0→95%는 90만). `threshold`는 최소 이 기준을 넘었다는 뜻입니다.",
      "when": "사용량 75% / 90% 기준 도달"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "days_remaining (3/1)",
        "expires_at"
      ],
      "event_type": "esim.expiring",
      "note": "`order_id`와 구매 시 보낸 `external_ref`로 eSIM을 주문과 연결하세요。",
      "when": "만료 3일 / 1일 전"
    },
    {
      "data_fields": [
        "threshold (25/10)",
        "balance",
        "currency"
      ],
      "event_type": "wallet.low_balance",
      "note": "`threshold`는 백분율(25/10)이며 실제 비율이 있을 때만 포함합니다. 없으면 필드를 생략하므로 `balance`를 직접 비교하세요. `threshold`에 VND 금액을 보내지 않습니다.",
      "when": "지갑 25% / 10% 경고 기준 도달"
    },
    {
      "data_fields": [
        "changes[] (package_code, change: price|availability|package, occurred_at)",
        "change_count"
      ],
      "event_type": "catalog.updated",
      "note": "조직마다 가격이 달라 가격은 포함하지 않습니다. `GET /v1/catalog/packages`로 조직 가격을 조회하세요. 짧은 구간(기본 10분)의 변경을 하나의 `changes[]` 웹훅으로 묶고 패키지당 한 항목만 남깁니다. 수백 개를 동기화해도 수백 웹훅이 발송되지 않습니다. 테스트와 실제 모두 적용합니다.",
      "when": "카탈로그 변경: 패키지 추가·수정·숨김, 기본 가격 변경 또는 품절"
    }
  ]
}
