Webhook Event

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

1.1 Event Subscription

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

  • 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

Header
Description

Chzzk-Event-Message-Id

메시지 식별자 ID

Chzzk-Event-Message-Timestamp

메시지 발송 시각 UTC datetime(RFC 3339)

Chzzk-Event-Message-Signature

HMAC signature, 치지직에서 보낸 메시지가 맞는지 검증 용도로 사용 필요

Chzzk-Event-Message-Type

message type 유형 현재 notification value로만 제공

Chzzk-Event-Message-Data-Type

이벤트 데이터 유형 (참조 : Event Types)

Chzzk-Event-Message-Version

message 형식 버전 message.version 값과 일치 (참조 : Notification Message)

Chzzk-Event-Message-Data-Version

eventType 에 따른 data 형식 버전 message.event.version 값과 일치 (참조 : Notification Message)

Chzzk-Event-Message-Retry

이벤트 재발송 횟수 정상적인 경우 보통 한 번의 메시지만 발송하지만, 알림을 수신 받는 곳에서 정상적으로 응답하지 않는다고 판단했을 경우 다시 한번 이벤트를 재발송함. Chzzk-Event-Message-Id헤더에 같은 id의 메시지가 여러 차례 재발송 될 수 있음 (참조 : Retry Policy) Note : 재발송 경우에만 Retry 헤더를 전달함

1.2.2 Notification Message

  • Webhook URL로 POST 요청을 전송하게 됩니다.

  • Reqeust body를 JSON parsing 하여 사용합니다.

  • JSON parsing을 완료할 경우 다음과 같은 형태의 메시지를 응답 받습니다.

예) 메시지 형태

Chzzk-Event-Message-Type : notification

{
  "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

Name
Type
Description

message

Message

이벤트 정보 및 내용을 포함

subscription

Subscription

이벤트를 수신 받는 client 의 metadata

Message

Name
Type
Description

messageId

String

메시지 식별자 id

version

String

message 형식 버전 Chzzk-Event-Message-Version 헤더 값과 동일

event

Event

이벤트 정보 이벤트 타입에 따라 데이터가 달라질 수 있음

Event

Name
Type
Description

eventTimeMillis

long

이벤트 발생시각 밀리 초 단위 (UTC)

version

String

Event data 형식 버전 Chzzk-Event-Message-Data-Version 헤더 값과 동일

eventType

String

이벤트 데이터 유형 (참조 : Event Types)

data

Data

이벤트 정보 이벤트 타입에 따라 데이터가 달라질 수 있음 (참조 : Event Types)

Subscription

Name
Type
Description

clientId

String

clientId

status

String

현재 구독상태 (DISABLED 경우엔 이벤트가 전달되지 않으므로 ENABLED가 기본값으로 전달됨)

method

Method

이벤트 전송 방법

Method

Name
Type
Description

methodType

String

Notification type의 경우 현재 WEBHOOK 값으로만 전달됨

url

String

Webhook URL

1.3 Event Types

Name
Type
Version
Description

Drop Reward Claim

drop_reward_claim

1

드롭스 리워드 아이템 신청 이벤트

1.3.1 drops_reward_claim

eventType이 drop_reward_claim으로 전달됩니다.

Event Data

Name
Type
Description

dropsClaimId

String

지급 신청 데이터의 고유한 식별자 ID

channelId

String

치지직 프로필을 사용하는 유저의 고유한 식별자 ID

dropsRewardId

String

신청한 리워드 아이템 ID

dropsCampaignId

String

진행하고 있는 캠페인 ID

dropsCategoryId

String

캠페인에 할당된 카테고리 ID

dropsCategoryName

String

캠페인에 할당된 노출 가능한 카테고리 이름

dropsClaimDate

String

지급 신청한 날짜 UTC datetime(RFC 3339)

1.4 Event Message Verification

  • 이벤트 메시지를 처리하기 전에 알림을 수신 받는 곳에서 반드시 치지직에서 보낸 메시지인지 확인하는 과정이 필요합니다.

  • 치지직 개발자 센터 > 서비스 client 등록 시 발급되는 client 의 secret key를 비밀키로 사용하여 HMAC-SHA256 서명을 생성합니다.

  • 아래의 코드 snippet을 확인하여 검증 구현이 필요합니다.

    • Javascript 에서는 헤더를 소문자로 표기해야 합니다.

    • Signature 가 매칭되지 않는다면, 2xx 상태코드가 아닌 4xx 상태코드 값으로 반환해야 합니다..

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

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

Last updated