기본 정보
Description
해피톡이 상담 이벤트를 고객사 수신 도메인으로 발송하는 Webhook 입니다. 고객사는 수신 URL 만 등록하면 됩니다.
상담방의 상태 변화와 메시지 송수신을 실시간으로 수신할 수 있습니다. 이벤트 그룹은 두 가지이며, 필요한 그룹만 선택해 구독할 수 있습니다.
| 이벤트 그룹 | 내용 | 규격 |
|---|---|---|
ROOM |
상담방 생성 · 상태 변경 · 상담사 변경 · 분류 변경 · 태그 변경 · 플래그 변경(구독별 적용) | 상담방 이벤트 |
CHAT |
고객 메시지 수신 · 상담사 메시지 발송 · 봇/자동 안내 메시지 포함 | 메시지 이벤트 |
시작하기
고객사가 준비할 것은 두 가지뿐입니다.
- 수신 URL — 해피톡이 POST 할 주소를 알려주시면 등록해 드립니다.
- 수신 서버 — POST 를 받아 HTTP 200 으로 응답하는 엔드포인트.
앱 키와 서명 검증용 시크릿은 해피톡이 발급해 별도 안전한 경로로 전달드립니다.
수신 URL 은 https:// 를 사용해 주세요.
본문에 상담 내용·고객 식별자가 포함되므로 평문 전송(http://)은 권장하지 않습니다.
공통 규격
요청 헤더
| Field Name | Type | Required | Description |
|---|---|---|---|
| Content-Type | String | Y | application/json (기본값 — 등록 시 추가 헤더를 따로 지정한 경우 달라질 수 있음) |
| x-mbi-app-signature | String | Y※ | 본문 전체를 앱 시크릿으로 서명한 값 (아래 참고) |
※ 구독별로 적용되는 항목이 있습니다.
ROOM 의 eventId · changedFields · flag · parameter1 ~ parameter10(data 내)와 CHAT 의 서명 헤더 · eventType · issuedAt · eventId · siteId(최상위) 필드는 웹훅 구독 단위로 적용됩니다. (ROOM 의 data.siteId 는 적용 여부와 무관하게 항상 전달됩니다.)
새로 등록하는 구독에는 기본으로 적용되며(Open API 로 구독을 해제한 뒤 다시 등록한 ROOM 구독도 새 구독입니다), 이전부터 사용 중인 구독은 요청하시면 담당자가 전환해 드립니다.
적용 전 구독에는 해당 항목이 전달되지 않으므로, 없을 수도 있는 값으로 처리해 주세요
(필수 전제로 역직렬화하면 적용 전 구독에서 파싱이 실패합니다).
| 항목 | 적용 전 구독 | 적용 후 구독 (새 구독 기본) |
|---|---|---|
| ROOM 본문 | eventType · issuedAt · appCredential · data |
왼쪽 + eventId · changedFields |
ROOM data |
기존 필드(상담방 이벤트 표에서 "구독별 적용" 표시가 없는 필드) | 왼쪽 + flag · parameter1 ~ parameter10 |
| ROOM 서명 헤더 | 있음 | 있음 |
| ROOM 발생 시점 | 플래그만 바뀐 경우는 대개 발송되지 않음(비교할 직전 상태가 없으면 발송될 수 있음) | 플래그 변경도 발송 |
| CHAT 본문 | id · roomId · roomEndMessage · type · userType · contents · contentsType · customerUid · msgId · sendDate |
왼쪽 + eventType · issuedAt · eventId · siteId |
| CHAT 서명 헤더 | 없음 | 있음 |
서명 검증
수신하신 본문 원문(raw body) 을 그대로 사용해 아래와 같이 계산한 값이
x-mbi-app-signature 헤더와 같은지 확인합니다.
언어별 예시
const crypto = require('crypto');
const expected = crypto
.createHmac('sha512', SECRET)
.update(rawBody) // JSON.parse 이전의 원문
.digest('base64');
const sig = req.headers['x-mbi-app-signature'];
// 헤더 존재·길이 동일성 선확인 (Buffer 기준 — 불일치 시 timingSafeEqual 예외 방지)
if (!sig || Buffer.from(sig).length !== Buffer.from(expected).length) return false;
// 타이밍 공격 방지를 위해 crypto.timingSafeEqual() 사용 권장
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
// 본문은 문자열로 디코딩하지 말고 **원 바이트** 그대로 서명 대상으로 쓰세요.
// (String 경유 시 charset 왕복이 끼어들어 한글 본문에서 검증이 실패할 수 있습니다)
byte[] rawBytes = request.getInputStream().readAllBytes();
Mac mac = Mac.getInstance("HmacSHA512");
mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA512"));
String expected = Base64.getEncoder().encodeToString(mac.doFinal(rawBytes));
String sig = request.getHeader("x-mbi-app-signature");
if (sig == null) return false; // 헤더 부재 = 거부
// 타이밍 공격 방지를 위해 MessageDigest.isEqual() 사용 권장 (Base64 는 ASCII)
boolean valid = MessageDigest.isEqual(
expected.getBytes(StandardCharsets.US_ASCII),
sig.getBytes(StandardCharsets.US_ASCII));
본문을 파싱한 뒤 다시 직렬화한 문자열로 검증하면 실패합니다.
키 순서·공백이 달라질 수 있으므로 파싱 전 원문을 보관해 검증에 사용하세요.
적용이 확인되면 서명 없는 요청을 거부해 주세요.
ROOM 요청은 적용 여부와 무관하게 항상 서명되므로 ROOM 은 처음부터 서명을 검증해 주세요. 적용 전 구독의 CHAT 요청에는 서명 헤더가 없어 허용이 필요합니다. CHAT 요청에 서명 헤더가 실려 오기 시작한 뒤 30분 정도 지나서(전환 전에 발행돼 재시도 중인 이벤트는 서명 없이 도착할 수 있습니다) CHAT 의 서명 헤더 부재·불일치 요청을 즉시 거부(4xx) 하도록 전환해 주세요. 이 엔드포인트에는 서명 외의 인증 수단이 없습니다.
응답 규약
성공 판정은 HTTP 200 만 입니다.
201·202·204 도 실패로 간주되어 재발송됩니다. 반드시 200 으로 응답해 주세요.
등록한 URL 에서 리다이렉트 없이 바로 200 을 반환해 주세요. 리다이렉트되면 최종 응답으로 성공을 판정하며,
301·302·303 에서는 POST 본문이 전달되지 않을 수 있습니다(예: 로그인 페이지로 리다이렉트되면 처리되지 않았는데도 성공으로 간주).
- 응답 본문 형식은 확인하지 않습니다(비어 있어도 됩니다).
- 수신 처리는 즉시 200 을 반환하고 비동기로 하시길 권장합니다 — 요청 타임아웃은 3초 입니다.
재시도와 전달 보장
| 단계 | 횟수 | 간격 |
|---|---|---|
| 최초 전송 | 1회 (즉시 재시도 없음 · 요청당 타임아웃 3초) | — |
| 지연 재발송 | 최대 2회 | 6초 → 12초 |
재발송을 모두 소진하면(무응답 기준 최소 약 27초) 해당 이벤트는 폐기됩니다.
간격은 최솟값입니다. 이벤트 처리량에 따라 재발송·도착이 더 늦어질 수 있습니다. 수신 서버가 30초 가까이 응답하지 못하면 그 사이 이벤트는 유실될 수 있습니다. 안정적인 수신을 위해 즉시 200 응답 후 비동기 처리를 권장합니다.
앱 인증정보(앱 키)를 비활성화·교체하는 동안에는 이벤트가 누락되거나, 이미 발행된 이벤트가 기존 시크릿으로 서명된 채 도착할 수 있습니다. 인증정보 재발급·교체와 서명 검증 전환 일정은 담당자와 맞춰 진행해 주세요.
해피톡 내부 사정으로 이벤트가 늦게 도착할 수 있습니다. 수신 시각을 이벤트 발생 시각으로 간주하지 말고
issuedAt 기준으로 정렬해 주세요.
중복 수신 처리
재발송 범위 안에서 같은 이벤트가 두 번 이상 도착할 수 있습니다(중복 수신 가능). 재발송을 모두 소진한 이벤트는 위와 같이 폐기되어 도착하지 않을 수 있으므로, 누락이 문제 되는 데이터는 조회 API 로 보정해 주세요.
본문의 eventId 를 기준으로 중복을 제거해 주세요.
- 재시도 구간에서도 같은 이벤트는 항상 같은
eventId를 갖습니다. - 유일성 범위는 이벤트 종류에 따라 다릅니다 —
ROOM의eventId는 수신자(구독) 단위로 유일하고,CHAT의eventId는 이벤트 단위로 유일합니다. 한 site 에CHAT구독이 여러 개인 경우 각 구독이 같은eventId를 받을 수 있으므로, 중복 제거는 구독(수신 URL) 단위로 수행해 주세요. - ⚠️ 한 site 에서 같은 수신 URL 을 두 개 이상의 구독에 등록하지 않는 것을 전제로 합니다.
그 구성에서는 본문이 사실상 같고(
issuedAt이 미세하게 다를 수 있습니다) 서명만 다른 요청이 같은 URL 로 2회 도착하는데,CHAT본문에는 어느 인증키로 서명했는지가 실려 있지 않아 수신 URL 단위 중복 제거로는 검증 가능한 쪽을 골라낼 수 없습니다. 같은 URL 로 여러 구독이 필요하시면 문의해 주세요. CHAT의msgId는 메시지 식별자이며 중복 제거 키가 아닙니다 —eventId를 사용하세요.
수신 URL 등록
수신 URL 과 필요한 이벤트 그룹(ROOM / CHAT)을 알려주시면 해피톡이 등록합니다.
등록 즉시 실제 상담 트래픽이 흐르므로, 수신 서버를 먼저 준비하신 뒤 요청해 주세요.
문의 사항은 담당자에게 회신 부탁드립니다.