웹훅
POST /v1/webhooks로 URL을 등록하고 PUT /v1/webhooks/{id}로 수정합니다. 생성 시 CHECK_HEALTH에 2xx가 필요합니다. API 키와 다른 전용 비밀 키로 이벤트를 서명합니다.
최종 기준은 REST입니다. 웹훅을 놓쳐도 GET /v1/orders?updated_since=…로 상태를 복구할 수 있도록 대사를 설계하세요.
webhook-events.ko.json이벤트 목록
| event_type | 발생 시점 | 주요 데이터 필드 |
|---|---|---|
order.completed | 주문 확정 및 eSIM 준비 완료 | order_idexternal_refitems[] (order_item_id, iccid) |
order.failed | 주문 미완료(발급 실패 등) | order_idexternal_refreason |
esim.delivered | eSIM이 발급되고 설치 데이터가 준비됨 `order_id`와 `external_ref`로 연결합니다. `lpa`는 POST 직전에 복호화하며 전송 기록에 평문으로 저장하지 않습니다. QR 이미지 URL은 없으므로 `lpa`로 생성하거나 `GET /v1/esims/{id}`에서 `qr_code_base64`를 조회하세요. | order_item_idorder_idexternal_reficcidlpa |
esim.activated | eSIM 설치 및 활성화(smdp enabled/in_use) `order_id`와 구매 시 보낸 `external_ref`로 eSIM을 주문과 연결하세요。 | order_item_idorder_idexternal_reficcidactivated_at |
esim.usage_threshold | 사용량 75% / 90% 기준 도달 `order_id`와 `external_ref`로 주문에 연결합니다. 새로 넘은 기준 중 가장 높은 값만 발송합니다(0→95%는 90만). `threshold`는 최소 이 기준을 넘었다는 뜻입니다. | order_item_idorder_idexternal_refthreshold (75/90)used_bytesdata_bytes |
esim.expiring | 만료 3일 / 1일 전 `order_id`와 구매 시 보낸 `external_ref`로 eSIM을 주문과 연결하세요。 | order_item_idorder_idexternal_refdays_remaining (3/1)expires_at |
wallet.low_balance | 지갑 25% / 10% 경고 기준 도달 `threshold`는 백분율(25/10)이며 실제 비율이 있을 때만 포함합니다. 없으면 필드를 생략하므로 `balance`를 직접 비교하세요. `threshold`에 VND 금액을 보내지 않습니다. | threshold (25/10)balancecurrency |
catalog.updated | 카탈로그 변경: 패키지 추가·수정·숨김, 기본 가격 변경 또는 품절 조직마다 가격이 달라 가격은 포함하지 않습니다. `GET /v1/catalog/packages`로 조직 가격을 조회하세요. 짧은 구간(기본 10분)의 변경을 하나의 `changes[]` 웹훅으로 묶고 패키지당 한 항목만 남깁니다. 수백 개를 동기화해도 수백 웹훅이 발송되지 않습니다. 테스트와 실제 모두 적용합니다. | changes[] (package_code, change: price|availability|package, occurred_at)change_count |
event_type: "ping"모든 `POST /v1/webhooks` 및 `PUT /v1/webhooks/{id}`에서 저장 전에 상태 확인.
`events`로 등록하거나 해제할 수 없습니다(`ping` 지정 시 `internal.validation_failed`). 모든 엔드포인트가 받습니다. 실제 봉투와 서명(`X-TKE-Webhook-Signature`, `-Old` 없음)을 사용하므로 검증 테스트에 활용하세요. 알 수 없는 `event_type`에도 2xx를 반환해야 합니다. 4xx면 생성·수정이 `webhook.health_check_failed`(422)로 실패하고 저장되지 않습니다. `ping`은 `GET /v1/webhooks/deliveries`에 없습니다.
등록 실패를 방지하려면 이 이벤트 유형을 처리하세요.
{
"event_id": "018f3c00-0000-7000-8000-0000000000c1",
"event_type": "ping",
"occurred_at": "2026-07-11T07:29:00Z",
"is_testmode": true,
"data": {
"message": "TongKho eSIM webhook health check"
}
}응답 형식 및 서명 헤더
모든 웹훅은 같은 봉투를 사용합니다. `HMAC-SHA256(webhook_secret, timestamp\nraw_body)`를 소문자 hex로 `X-TKE-Webhook-Signature`에 서명합니다. 24시간 교체 구간에는 `X-TKE-Webhook-Signature-Old`도 보냅니다. 최소 1회 전송이므로 `event_id`로 중복 제거하세요.
{
"event_id": "018f3c00-0000-7000-8000-0000000000b0",
"event_type": "order.completed",
"occurred_at": "2026-07-11T07:30:00Z",
"is_testmode": false,
"data": {
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"external_ref": "partner-cart-9931",
"items": [
{
"order_item_id": "018f2b00-0000-7000-8000-000000000001",
"iccid": "8984012345678901234"
}
]
}
}| 필드 | 설명 |
|---|---|
data | 이벤트 유형별 데이터입니다(아래 참고). |
event_id | 이벤트별로 고유하고 고정된 UUID입니다(중복 제거). |
event_type | 이벤트 유형입니다. |
is_testmode | 테스트 키에서 발생했으면 true입니다。 |
occurred_at | ISO-8601 UTC. |
카탈로그 동기화: catalog.updated
요금제 추가·수정·숨김·기본가·재고 변경 시 발생하며 변경 사항을 다음 배열로 모읍니다: changes[] 배열에 수백 개 변경이 있어도 단 한 번 의 전송만 각 엔드포인트에 보냅니다.
{
"event_id": "018f3c00-0000-7000-8000-0000000000c1",
"event_type": "catalog.updated",
"occurred_at": "2026-07-11T08:10:00Z",
"is_testmode": false,
"data": {
"changes": [
{
"package_code": "THAY-BANG-MA-GOI-CUA-BAN",
"change": "price",
"occurred_at": "2026-07-11T08:02:11Z"
},
{
"package_code": "VN_10GB_30D",
"change": "availability",
"occurred_at": "2026-07-11T08:04:52Z"
},
{
"package_code": "JP_3GB_7D",
"change": "package",
"occurred_at": "2026-07-11T08:09:30Z"
}
],
"change_count": 3
}
} 판매가는 조직별로 다릅니다 . 공통 가격은 부정확하고 정보를 노출합니다. 이 이벤트는 동기화 신호입니다 : 다음 호출로 GET /v1/catalog/packages?market=… 가격을 갱신하세요.
change 허용 값 3개: price (기본가 변경), availability (재고 있음/없음), package (신규/수정/숨김). 알 수 없는 값도 동기화를 시작하세요.
서명, 재시도 및 중복 제거 기준 event_id 는 일반 이벤트와 같습니다. 테스트와 운영은 다음 단위로 분리합니다: is_testmode.
웹훅 서명 검증
X-TKE-Signature는 webhook_secret과 본문 원본 바이트로 계산한 HMAC-SHA256입니다.
signing_string = timestamp + "\n" + raw_body # timestamp = header X-TKE-Webhook-Timestamp
signature = HMAC_SHA256(webhook_secret, signing_string) → hex lowercase
# 재전송 공격 방지를 위해 ±300초 확인.
# 수신 바이트 그대로 서명하고 JSON을 다시 직렬화하지 마세요.재시도 정책
2xx 외 응답 또는 5초 초과 시 24 giờ 동안 지수 간격으로 재시도합니다: 1 phút → 5 phút → 30 phút → 2 giờ → 6 giờ.
웹훅 핵심 지침 4가지
필드 동작 참고
esim.delivered.lpa 는 POST 직전에 복호화되며 평문 저장하지 않습니다. LPA 또는 GET /v1/esims/{id}로 QR을 만드세요.
esim.usage_threshold: 는 새로 넘은 가장 높은 임계값만 전송합니다(0→95%는 threshold=90).
wallet.low_balance.threshold 는 백분율(25/10)이며 VND 금액이 아닙니다.
catalog.updated 는 가격·세부 재고 없이 요금제 코드와 변경 유형만 포함합니다.