<!-- Source: https://docs.infraio.xyz/ko/webhooks/overview -->
<!-- Last updated: 2026-10-04 -->

# 웹훅 — 개요

웹훅은 **권위 있는** 신호입니다. 브라우저 콜백(`onSuccess`)과 대시보드 뷰는
편의 사항이며, 웹훅이 그라운드 트루스입니다.

## 전달 보장

- **At least once.** 단일 이벤트는 가맹점 서버가 타임아웃 내에 2xx를
  반환하지 않으면 최대 **6회**까지 전달될 수 있습니다. 핸들러를 멱등성
  있게 만드세요 — `X-Delivery`로 중복을 제거하세요(페이로드에는 `event_id`
  필드가 없으며, 안정적인 전달 UUID가 멱등성 키입니다).
- **HTTP 요청당 하나의 이벤트.** 배치 없음.
- **엔드포인트별 격리.** 여러 엔드포인트가 등록되어 있다면 각각 자체
  전달 및 재시도 트랙을 가집니다. 느린 엔드포인트 하나가 다른 엔드포인트를
  지연시키지 않습니다.
- **서명됨.** 모든 페이로드는 `X-Signature` 헤더(그리고 로테이션 후
  24시간 윈도우 동안 `X-Signature-Prev`도)를 가집니다. 본문으로 무엇이든
  하기 전에 검증하세요. [서명 검증](https://docs.infraio.xyz/ko/webhooks/signature-verification)을
  참조하세요.

## 구독 가능한 이벤트 타입

| 이벤트 | 발생 시점 |
| --- | --- |
| `payment.settled` | 온체인 송금이 체인의 확인 수를 클리어함. **이것을 사용하여 주문을 결제 완료로 표시하세요.** |
| `payment.failed` | 법정화폐 결제가 결제 제공자에 의해 거부됨. 암호화폐 타임아웃에는 발생하지 않음 — 그것은 대신 `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`은 선택적 가맹점 메모입니다. |

### 계획됨 (출시 예정)

> **Note:**
>
> **출시 예정.** 이 이벤트들은 아직 사용할 수 없는 정기 청구서와 구독에
> 속합니다. 위의 구독 가능한 표에 **포함되어 있지 않으며** 현재는 구독할 수
> 없습니다. [정기 청구서](https://docs.infraio.xyz/ko/guides/recurring-invoices)를 참고하세요.

| 계획된 이벤트 | 발생 시점… |
| --- | --- |
| `subscription.created` | 구독이 생성될 때. |
| `invoice.created` | 청구 주기의 청구서가 생성될 때. |
| `invoice.paid` | 청구서가 결제될 때. |
| `subscription.past_due` | 청구서가 납기일을 지나도 미결제 상태일 때. |
| `subscription.canceled` | 구독이 취소될 때. |

대시보드의 엔드포인트 양식에도 동일한 이벤트가 나열됩니다. 존재하지 않는
이벤트를 구독하면 엔드포인트를 저장할 때 거부됩니다.

> **Note:**
>
> **테스트 이벤트는 구독할 수 없습니다.** 대시보드의 엔드포인트별 **Send Test**
> 버튼은 해당 엔드포인트 하나에만 `webhook.test.ping` 이벤트를 재시도 없이
> 즉시 전송합니다. 위 카탈로그에는 나타나지 않습니다. 구독해서가 아니라
> 등록된 엔드포인트가 있기 때문에 받는 것입니다.

> **Note:**
>
> 처리하는 이벤트만 구독하세요. 각 엔드포인트는 자체 이벤트 필터를
> 가집니다. 와일드카드 `"*"`는 "미래에 추가될 것을 포함한 모든 이벤트"를
> 의미합니다. 더 적은 이벤트를 구독하면 핸들러가 단순해지고 엔드포인트에 오류가
> 있을 때 재시도도 줄어듭니다.

## 페이로드 + 헤더

**HTTP 본문은 이벤트별 데이터 객체 자체입니다.** Stripe 스타일의 외부
envelope 없음 — 이벤트 타입, 전달 ID, 발신 타임스탬프와 같은 필드는
**헤더**에 있습니다. `payment.settled`의 경우 본문은 다음과 같습니다.

```json
{
  "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":          { /* 이벤트별 */ }
}
```

다른 이벤트는 자체 필드를 가집니다. 필드 이름은 안정적입니다(소문자 스네이크
케이스); 온체인 트랜잭션 해시는 항상 `tx_hash`입니다.

`tx_hash`는 네트워크 고유 형식의 트랜잭션 식별자입니다(EVM 체인은 `0x…`, TRON·Solana·TON은 네이티브 해시 또는 서명). TRON, Solana, TON에서는 구매자가 가맹점의 트레저리 지갑으로 직접 결제하므로 `deposit_address`가 없을 수 있으며, `confirmations`는 [체인 및 자산](https://docs.infraio.xyz/ko/concepts/chains)을 따릅니다.

### 인바운드 요청의 헤더

```http
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`와 미러링(동일한 값). 모든 전달에 설정됨. |
| `X-Timestamp` | 시도가 전송된 유닉스 초. 페이로드에 서명되므로 캡처된 `(body, X-Signature)` 쌍이 무한정 재생될 수 없습니다 — 타임스탬프가 허용 윈도우 밖인 전달을 거부하세요. |
| `X-Signature` | `HMAC-SHA256(secret, X-Timestamp + "." + raw_body)`의 `sha256=<hex>`. [서명 검증](https://docs.infraio.xyz/ko/webhooks/signature-verification) 참조. |
| `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번째 시도가 실패하면 전달은 **Failed**로 표시되고 계정 이메일로
알림이 전송됩니다. 실패한 이벤트는 대시보드의 **Developers →
Webhooks → Delivery history** 패널에서 재생할 수 있습니다. 각
재생은 자체 `X-Delivery`를 가진 새 전달입니다.

## 엔드포인트 등록

[가맹점 대시보드](https://app.infraio.xyz)에서:

1. **Developers → Webhooks** → **+ Add endpoint**
2. URL 붙여넣기 — `https://…`만(일반 HTTP는 거부됨; 생성 양식도
   `localhost`, 사설 IP 범위, userinfo가 있는 URL을 차단함)
3. 구독할 이벤트 선택(또는 `*` 모두)
4. 환경 선택 — **test** 또는 **live**(각각 자체 시크릿을 가지며 절대
   교차되지 않음)
5. 저장 → 대시보드가 서명 시크릿(`whsec_…`)을 **한 번** 표시합니다.
   서버 측에 저장하세요 — 다음 두 기능에 필요합니다.

**가맹점당 환경당 최대 10개의 엔드포인트**(예: 프로덕션 이행용 하나,
스테이징 미러링용 하나, Slack 알림용 하나)를 등록할 수 있습니다. 각각
자체 재시도 상태와 시크릿을 가집니다.

## 각 엔드포인트의 라이프사이클 액션

각 엔드포인트 카드의 ⋮ 메뉴는 다음을 노출합니다.

- **Edit** — URL, 설명 또는 구독 목록 변경. 새 URL은 생성과 동일한
  `https://`/SSRF 규칙으로 재검증됩니다.
- **Send Test** — 현재 시크릿으로 서명된 `webhook.test.ping` envelope를
  동기적으로 POST합니다. 대시보드는 HTTP 상태, 지연, 응답의 512바이트
  스니펫을 표시합니다. 테스트 핑은 재시도되지 않으므로 답변이 즉시 반환됩니다.
- **Rotate Secret** — 새 시크릿을 생성합니다. 이전 시크릿은 **24시간**
  동안 유효합니다(전달은 `X-Signature`와 `X-Signature-Prev`를 모두
  포함하여 윈도우 동안 어느 쪽 키를 실행 중인 검증자도 재배포 중에
  이벤트를 계속 받을 수 있게 합니다).
- **Reveal Secret** — 기존 시크릿을 다시 표시합니다. 새 2FA 검증으로
  게이팅되고 감사 로그에 기록됨. 사본을 분실했고 Rotate가 허용되지 않는
  경우에만 사용하세요.
- **Enable / Disable** — 전달 이력을 잃지 않고 엔드포인트를 켜거나 끕니다.
  비활성화된 엔드포인트는 대시보드에 남아 있지만 새 전달을 받지 않습니다.
- **Delete** — 영구적입니다. 나중에 재활성화할 수 있는 경우 Disable을
  사용하세요.

## 핸들러를 위한 팁

1. **빠르게 2xx 반환.** 무거운 작업을 수행하기 전에 `200 OK`로 응답하세요 —
   이행을 백그라운드 작업으로 넘기세요. 시도당 타임아웃은 **10초**입니다.
   더 오래 응답을 보류하면 재시도가 트리거됩니다. 타임아웃은 플랫폼
   측이며 가맹점이 구성할 수 없습니다 — 핸들러가 진정으로 더 많은 시간이
   필요한 경우 지원팀에 문의하세요.
2. **`X-Delivery`로 중복 제거**(또는 `Idempotency-Key` — 동일한 값).
   2xx를 반환해도 업스트림 프록시가 연결을 끊고 재시도를 트리거할 수
   있습니다. 전달 ID는 동일한 전달 행의 모든 재시도에서 안정적이므로
   올바른 키입니다.
3. **알 수 없는 이벤트 타입을 허용.** 새 이벤트가 나타날 수 있습니다.
   4xx 대신 200을 반환하고 no-op 하세요. 그렇지 않으면 해당 전달이 계속 재시도됩니다.
4. **비즈니스 로직 옆에 `X-Delivery` 로깅.** 무언가 잘못되면 그것이
   저희 측과 가맹점 측 사이의 조인 키입니다.

## 다음 단계

- [서명 검증](https://docs.infraio.xyz/ko/webhooks/signature-verification) — 정확한 알고리즘 +
  리플레이 보호 패턴.
- [개념 → 세션](https://docs.infraio.xyz/ko/concepts/sessions) — 각 이벤트 발생 시 세션이 어떤
  상태에 있는지.
