# 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(이벤트 발생 시각)는 메시지 전송시각이 아닌 재발송 전 이벤트 전송 시작 시간을 기반으로 전송합니다.

