# Webhook Event

치지직 Webhook을 이용하여 이벤트 알림(Notification Message)을 받는 방법을 안내하기 위한 가이드 문서입니다.

## 1.1 Event Subscription

이벤트 발생 알림을 받기 위해 이벤트를 구독할 수 있는 기능을 제공합니다.&#x20;

* Webhook을 이용하여 이벤트 알림(Notification Message)을 받는 방법을 우선적 제공하고 있습니다.
* 치지직 개발자 센터 사용 신청과 이벤트 알림을 받을 Webhook URL 등록이 완료 되어있는지 확인해 주세요.

## 1.2 Handling Webhook Event

* POST 요청 Webhook URL로 이벤트가 전송되게 됩니다.
* Webhook URL은 반드시 SSL 사용과 443 port가 열려있어야 합니다. (HTTPS 필수)
* Webhook POST 요청 응답은 반드시 수 초 이내 응답이 전달되어야 하며, 메시지 발송이 제대로 가지 않았다고 판단이 되면 최대 3번의 메시지 재발송이 이루어집니다. (자세한 retry 관련 설명은 Request Header의 Retry 헤더관련 내용 참조)
* Webhook POST 요청 응답 상태 코드는 2XX 형태의 코드여야 합니다. 그 외는 모두 실패로 간주합니다.

### 1.2.1 Request Header

<table><thead><tr><th width="327">Header</th><th>Description</th></tr></thead><tbody><tr><td>Chzzk-Event-Message-Id</td><td>메시지 식별자 ID</td></tr><tr><td>Chzzk-Event-Message-Timestamp</td><td>메시지 발송 시각 UTC datetime(RFC 3339)</td></tr><tr><td>Chzzk-Event-Message-Signature</td><td>HMAC signature, 치지직에서 보낸 메시지가 맞는지 검증 용도로 사용 필요</td></tr><tr><td>Chzzk-Event-Message-Type</td><td>message type 유형<br>현재 <code>notification</code> value로만 제공</td></tr><tr><td>Chzzk-Event-Message-Data-Type</td><td>이벤트 데이터 유형 (참조 : Event Types)</td></tr><tr><td>Chzzk-Event-Message-Version</td><td>message 형식 버전<br><code>message.version</code> 값과 일치<br>(참조 : Notification Message)</td></tr><tr><td>Chzzk-Event-Message-Data-Version</td><td>eventType 에 따른 data 형식 버전<br><code>message.event.version</code> 값과 일치<br>(참조 : Notification Message)</td></tr><tr><td>Chzzk-Event-Message-Retry</td><td>이벤트 재발송 횟수<br>정상적인 경우 보통 한 번의 메시지만 발송하지만, 알림을 수신 받는 곳에서 정상적으로 응답하지 않는다고 판단했을 경우 다시 한번 이벤트를 재발송함.<br><code>Chzzk-Event-Message-Id</code>헤더에 같은 id의 메시지가 여러 차례 재발송 될 수 있음<br>(참조 : Retry Policy)<br><strong>Note :</strong> 재발송 경우에만 Retry 헤더를 전달함</td></tr><tr><td></td><td></td></tr></tbody></table>

### 1.2.2 Notification Message

* Webhook URL로 POST 요청을 전송하게 됩니다.
* Reqeust body를 JSON parsing 하여 사용합니다.
* JSON parsing을 완료할 경우 다음과 같은 형태의 메시지를 응답 받습니다.

예) 메시지 형태

`Chzzk-Event-Message-Type : notification`

```json
{
  "message": {
    "messageId": "eafe79192ab427be4e85e5a825c980af",
    "version": "1",
    "event": {
      "eventTimeMillis": 1722477515159,
      "version": "1",
      "eventType": "drop_reward_claim",
      "data": {
        "dropsClaimId": "97",
        "channelId": "${channelId}",
        "dropsRewardId": "2",
        "dropsCampaignId": "c91f66d879fb96e39d2f44e0d6094e8a",
		"dropsCategoryId": "CATEGORY_CHZZK",
        "dropsCategoryName": "치지직",
        "dropsClaimDate": "2024-08-01T01:58:33Z"
      }
    }
  },
  "subscription": {
    "clientId": "${clientId}",
    "status": "ENABLED",
    "method": {
      "methodType": "WEBHOOK",
      "url": "${Webhook URL}"
    }
  }
}

```

**Notification Message fields**

<table><thead><tr><th width="174">Name</th><th width="138">Type</th><th>Description</th></tr></thead><tbody><tr><td>message</td><td>Message</td><td>이벤트 정보 및 내용을 포함</td></tr><tr><td>subscription</td><td>Subscription</td><td>이벤트를 수신 받는 client 의 metadata</td></tr><tr><td></td><td></td><td></td></tr></tbody></table>

**Message**

<table><thead><tr><th width="172">Name</th><th width="138">Type</th><th>Description</th></tr></thead><tbody><tr><td>messageId</td><td>String</td><td>메시지 식별자 id</td></tr><tr><td>version</td><td>String</td><td>message 형식 버전 Chzzk-Event-Message-Version 헤더 값과 동일</td></tr><tr><td>event</td><td>Event</td><td>이벤트 정보<br>이벤트 타입에 따라 데이터가 달라질 수 있음</td></tr><tr><td></td><td></td><td></td></tr></tbody></table>

**Event**

<table><thead><tr><th width="171">Name</th><th width="141">Type</th><th>Description</th></tr></thead><tbody><tr><td>eventTimeMillis</td><td>long</td><td>이벤트 발생시각 밀리 초 단위 (UTC)</td></tr><tr><td>version</td><td>String</td><td>Event data 형식 버전 Chzzk-Event-Message-Data-Version 헤더 값과 동일</td></tr><tr><td>eventType</td><td>String</td><td>이벤트 데이터 유형 (참조 : Event Types)</td></tr><tr><td>data</td><td>Data</td><td>이벤트 정보<br>이벤트 타입에 따라 데이터가 달라질 수 있음 (참조 : Event Types)</td></tr><tr><td></td><td></td><td></td></tr></tbody></table>

**Subscription**

<table><thead><tr><th width="172">Name</th><th width="141">Type</th><th>Description</th></tr></thead><tbody><tr><td>clientId</td><td>String</td><td>clientId</td></tr><tr><td>status</td><td>String</td><td>현재 구독상태<br>(DISABLED 경우엔 이벤트가 전달되지 않으므로 ENABLED가 기본값으로 전달됨)</td></tr><tr><td>method</td><td>Method</td><td>이벤트 전송 방법</td></tr><tr><td></td><td></td><td></td></tr></tbody></table>

**Method**

<table><thead><tr><th width="173">Name</th><th width="140">Type</th><th>Description</th></tr></thead><tbody><tr><td>methodType</td><td>String</td><td>Notification type의 경우 현재 WEBHOOK 값으로만 전달됨</td></tr><tr><td>url</td><td>String</td><td>Webhook URL</td></tr><tr><td></td><td></td><td></td></tr></tbody></table>

&#x20;

## 1.3 Event Types

<table><thead><tr><th width="199">Name</th><th width="176">Type</th><th width="85">Version</th><th>Description</th></tr></thead><tbody><tr><td>Drop Reward Claim</td><td>drop_reward_claim</td><td>1</td><td>드롭스 리워드 아이템 신청 이벤트</td></tr><tr><td></td><td></td><td></td><td></td></tr></tbody></table>

### 1.3.1 drops\_reward\_claim

eventType이 drop\_reward\_claim으로 전달됩니다.

&#x20;

**Event Data**

<table><thead><tr><th width="203">Name</th><th width="174">Type</th><th>Description</th></tr></thead><tbody><tr><td>dropsClaimId</td><td>String</td><td>지급 신청 데이터의 고유한 식별자 ID</td></tr><tr><td>channelId</td><td>String</td><td>치지직 프로필을 사용하는 유저의 고유한 식별자 ID</td></tr><tr><td>dropsRewardId</td><td>String</td><td>신청한 리워드 아이템 ID</td></tr><tr><td>dropsCampaignId</td><td>String</td><td>진행하고 있는 캠페인 ID</td></tr><tr><td>dropsCategoryId</td><td>String</td><td>캠페인에 할당된 카테고리 ID</td></tr><tr><td>dropsCategoryName</td><td>String</td><td>캠페인에 할당된 노출 가능한 카테고리 이름</td></tr><tr><td>dropsClaimDate</td><td>String</td><td>지급 신청한 날짜 UTC datetime(RFC 3339)</td></tr><tr><td></td><td></td><td></td></tr></tbody></table>

&#x20;

## 1.4 Event Message Verification

* 이벤트 메시지를 처리하기 전에 알림을 수신 받는 곳에서 반드시 치지직에서 보낸 메시지인지 확인하는 과정이 필요합니다.
* 치지직 개발자 센터 > 서비스 client 등록 시 발급되는 client 의 secret key를 비밀키로 사용하여 HMAC-SHA256 서명을 생성합니다.
* 아래의 코드 snippet을 확인하여 검증 구현이 필요합니다.
  * Javascript 에서는 헤더를 소문자로 표기해야 합니다.
  * Signature 가 매칭되지 않는다면, 2xx 상태코드가 아닌 4xx 상태코드 값으로 반환해야 합니다..

`Javascript`

```javascript
const crypto = require('crypto');
const express = require('express');
const app = express();

// Notification request headers
const CHZZK_MESSAGE_ID = 'CHZZK-EVENT-MESSAGE-ID'.toLowerCase();
const CHZZK_MESSAGE_TIMESTAMP = 'CHZZK-EVENT-MESSAGE-TIMESTAMP'.toLowerCase();
const CHZZK_MESSAGE_SIGNATURE = 'CHZZK-EVENT-MESSAGE-SIGNATURE'.toLowerCase();
const CHZZK_MESSAGE_TYPE = 'CHZZK-EVENT-MESSAGE-TYPE'.toLowerCase();
const CHZZK_MESSAGE_TYPE_NOTIFICATION = 'NOTIFICATION'.toLowerCase();

const HMAC_PREFIX = 'sha256=';

......

    let secret = getSecret();
    let message = getHmacMessage(req);
    let hmac = HMAC_PREFIX + getHmac(secret, message);

    if (true === verifyMessage(hmac, req.headers[CHZZK_MESSAGE_SIGNATURE])) {

        let notification = JSON.parse(req.body);

        if (CHZZK_MESSAGE_TYPE_NOTIFICATION === req.headers[CHZZK_MESSAGE_TYPE]) {
            console.log(JSON.stringify(notification));
            res.sendStatus(200);
        }
    } else {
        console.log('403');
        res.sendStatus(403);
    }
});

function getSecret() {
    // client 등록 시 발급되는 secret key를 비밀키로 사용 (아래는 예시 실제 시크릿키 아님)
    return "${secretKey}";
}


function getHmacMessage(request) {
    return (request.headers[CHZZK_MESSAGE_ID] +
        request.headers[CHZZK_MESSAGE_TIMESTAMP] +
        request.body);
}


function getHmac(secret, message) {
    return crypto.createHmac('sha256', secret)
        .update(message)
        .digest('hex');
}


function verifyMessage(hmac, verifySignature) {
    return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(verifySignature));
}

```

`Java`

```java
public class WebhookNotificationSubscriptionTest {

    private static final String CHZZK_MESSAGE_ID = "Chzzk-Event-Message-Id";
    private static final String CHZZK_MESSAGE_TIMESTAMP = "Chzzk-Event-Message-Timestamp";
    private static final String CHZZK_MESSAGE_SIGNATURE = "Chzzk-Event-Message-Signature";
    private static final String CHZZK_MESSAGE_TYPE = "Chzzk-Event-Message-Type";
    private static final String CHZZK_MESSAGE_TYPE_NOTIFICATION = "notification";
    private static final String HMAC_PREFIX = "sha256=";
    private static final String HMAC_ALGORITHM = "HmacSHA256";

    public void handleEvent(String body, HttpServletRequest httpServletRequest) throws NoSuchAlgorithmException, InvalidKeyException {

        String secret = getSecret();
        String message = getHmacMessage(httpServletRequest, body);
        String hmac = HMAC_PREFIX + generateHmac(secret, message);

        if (verifyMessage(hmac, httpServletRequest.getHeader(CHZZK_MESSAGE_SIGNATURE))) {
            if (CHZZK_MESSAGE_TYPE_NOTIFICATION.equals(httpServletRequest.getHeader(CHZZK_MESSAGE_TYPE))) {
                System.out.println("success: " + body);
                                // Body Json Parsing
            }
        } else {
            // 검증되지 않은 메시지 확인
        }
    }

    private String getSecret() {
        return "${secretKey}";
    }

    private String getHmacMessage(HttpServletRequest httpServletRequest, String body) {
        return httpServletRequest.getHeader(CHZZK_MESSAGE_ID) +
            httpServletRequest.getHeader(CHZZK_MESSAGE_TIMESTAMP) +
            body;
    }


    private String generateHmac(String secret, String message) throws NoSuchAlgorithmException, InvalidKeyException {
        Mac mac = Mac.getInstance(HMAC_ALGORITHM);
        SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
        mac.init(secretKeySpec);
        byte[] hmacBytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
        return bytesToHex(hmacBytes);
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    private boolean verifyMessage(String hmac, String verifySignature) {
        return hmac.equals(verifySignature);
    }
}

```

### 1.4.1 Retry Policy

이벤트 발송에 문제가 생겼을 경우(응답 속도가 느리거나, 5xx에러 발생 시) notification message 재발송이 정상적으로 전송될 때까지 최대 3회 발송합니다.

`Chzzk-Event-Message-Retry` 헤더를 참고하여 재발송에 대한 이벤트 확인 및 재발송에 대한 처리(중복 메시지 처리) 구현이 필요합니다.\
\*이벤트 재발송의 eventTimeMillis(이벤트 발생 시각)는 메시지 전송시각이 아닌 재발송 전 이벤트 전송 시작 시간을 기반으로 전송합니다.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chzzk.gitbook.io/chzzk/drops/webhook.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
