WebSocket 이벤트
WebSocket으로 명령어 사용, 룰렛 결과 등 채널 이벤트를 실시간으로 받는 방법입니다.
목차
WebSocket으로 연결하면 동의한 채널에서 일어나는 룰렛 결과, 명령어 사용 같은 이벤트를 실시간으로 받습니다. 연결에는 events:subscribe 권한이 필요합니다.
연결
액세스 토큰을 실어 WebSocket으로 연결합니다. 주소는 기본 주소의 https://를 wss://로 바꾸고 /ws를 붙인 것입니다.
/api/ v1/ developer/ ws권한: events:subscribe요청
토큰은 둘 중 한 가지 방법으로 보냅니다. 둘 다 보내면 access_token 쿼리를 씁니다.
| 이름 | 위치 | 타입 | 필수 | 설명 |
|---|---|---|---|---|
access_token |
query | string | 둘 중 하나 | 액세스 토큰. 브라우저의 WebSocket은 헤더를 붙일 수 없으므로 이 방법을 씁니다 |
Authorization |
header | string | 둘 중 하나 | Bearer ACCESS_TOKEN. 헤더를 붙일 수 있는 클라이언트에서 씁니다 |
wss://jamkubot.com/api/v1/developer/ws?access_token=ACCESS_TOKEN
주소에 담긴 토큰은 접속 기록에 남을 수 있습니다. 헤더를 쓸 수 있는 환경이면 헤더로 보내세요.
응답
연결되면 서버가 첫 메시지로 connected를 보냅니다.
| 필드 | 타입 | 설명 |
|---|---|---|
type |
string | 항상 connected |
streamer_uid |
string | 토큰이 연결된 채널 UID |
events |
array | 이 연결로 받을 수 있는 이벤트 타입. 알파벳순이며 연결할 때의 권한 기준입니다 |
{
"type": "connected",
"streamer_uid": "0123456789abcdef0123456789abcdef",
"events": ["attendance", "command", "optional_feature", "overlay_text", "point_use", "run_roulette", "run_roulette2"]
}
오류
서버는 연결을 받은 뒤 바로 닫는 방식으로 거절합니다. 클라이언트는 연결 종료 코드로 확인합니다.
| 종료 코드 | 원인 |
|---|---|
4401 |
토큰이 없거나, 유효하지 않거나(만료·갱신으로 바뀜·연결 해제 포함), events:subscribe 권한이 없음 |
같은 IP 주소에서 1분에 60번 넘게 연결하거나 동시에 32개 넘게 열면 연결을 받지 않습니다. 이때는 종료 코드 없이 핸드셰이크가 HTTP 403 으로 실패하니 잠시 뒤 다시 연결합니다.
메시지 흐름
메시지는 모두 JSON 텍스트입니다. ←는 서버가 보내는 메시지, →는 앱이 보내는 메시지입니다.
← {"type": "connected", "streamer_uid": "0123456789abcdef0123456789abcdef", "events": ["run_roulette", "run_roulette2"]}
← {"streamer_uid": "0123456789abcdef0123456789abcdef", "event": {"type": "run_roulette2", "roulette_name": "간식 룰렛", "...": "..."}}
→ {"type": "ping"}
← {"type": "pong"}
| 방향 | 메시지 | 보내는 때 |
|---|---|---|
| 서버 → 앱 | connected |
연결 직후 한 번. 필드는 연결 응답을 참고하세요 |
| 서버 → 앱 | 이벤트 프레임 | 채널에서 받을 수 있는 이벤트가 생길 때마다 |
| 앱 → 서버 | {"type": "ping"} |
연결과 권한을 확인할 때 |
| 서버 → 앱 | {"type": "pong"} |
ping을 받았고 토큰이 아직 유효할 때 |
이벤트 프레임의 형식입니다.
| 필드 | 타입 | 설명 |
|---|---|---|
streamer_uid |
string | 이벤트가 생긴 채널 UID |
event |
object | 이벤트 본문. event.type으로 종류를 구분합니다. 필드는 이벤트 페이로드를 참고하세요 |
- 이벤트 프레임에는 이벤트 ID나 발생 시각이 없습니다.
- 채팅 한 번으로 이벤트가 여러 개(예:
point_use,run_roulette,run_roulette2,command) 발행될 수 있으며, 서로 다른 이벤트의 도착 순서는 보장하지 않습니다. - 앱이 보내는 메시지 중
{"type": "ping"}만 처리하고, 다른 JSON 메시지는 무시합니다. 구독할 이벤트를 고르거나 수신을 확인하는 메시지는 없습니다. - JSON이 아닌 텍스트나 바이너리 메시지를 보내면 서버가 연결을 끊습니다.
이벤트 종류
받을 수 있는 이벤트는 토큰의 권한 조합으로 정해집니다. 앱에는 봇과 잼쿠봇 서버가 만든 이벤트만 전달되며, 외부 프로그램이 오버레이용 이벤트 채널에 직접 보낸 이벤트는 오지 않습니다.
| 권한 | 받는 이벤트 |
|---|---|
events:subscribe |
run_roulette, run_roulette2 |
events:subscribe + commands:read |
위 두 이벤트와 command, point_use, attendance, optional_feature, overlay_text |
명령어 관련 이벤트에 필요한 권한은 REST 명령어 조회 권한인 commands:read입니다. 명령어 수정 권한 commands:write와 혼동하지 마세요. 권한 목록은 권한 (Scope)에 있습니다.
연결한 뒤 스트리머가 더 좁게 다시 동의하거나 앱의 권한 선언이 줄면 그때부터 빠진 권한의 이벤트는 오지 않습니다. connected.events는 다시 보내지 않습니다. events:subscribe가 빠지면 연결이 4401로 닫힙니다.
| 이벤트 | 발행 시점 |
|---|---|
run_roulette |
룰렛이 한 번 돌 때마다. 연차면 회차마다 |
run_roulette2 |
룰렛 실행 한 번이 끝났을 때 결과를 모아 한 번 |
command |
커스텀 명령어, !sr 노래 신청, !문방구 뽑기를 실행했을 때 |
point_use |
커스텀 명령어나 !문방구 뽑기로 포인트가 빠지거나 들어왔을 때 |
attendance |
출석 체크에 성공했을 때 |
optional_feature |
포인트가 0인 커스텀 명령어에 출석 체크가 아닌 추가 기능 값이 있을 때 |
overlay_text |
현재 발행하는 곳이 없음 |
overlay_text는 허용 목록과 connected.events에는 있지만, 지금 잼쿠봇에는 이 타입을 발행하는 기능이 없습니다.
이벤트 페이로드
아래는 이벤트 프레임의 event에 담기는 본문입니다. 시청자가 친 채팅 원문(user_chat, use_command)이 가림 없이 들어 있으니 저장하거나 화면에 띄울 때 주의하세요.
run_roulette
룰렛이 한 번 돌 때마다 발행됩니다. 명령어·후원으로 돌린 룰렛과 대시보드의 룰렛 테스트가 모두 해당하며, 연차면 회차마다 하나씩 옵니다.
| 필드 | 타입 | 설명 |
|---|---|---|
type |
string | run_roulette |
roulette_name |
string | 룰렛 이름 |
user_name |
string | 돌린 사람의 닉네임. 익명 후원은 익명의 후원자, 대시보드 테스트는 테스트유저 또는 테스트에서 고른 시청자 닉네임 |
data |
array | 룰렛 항목 이름 전체(string). 룰렛에 저장된 순서입니다 |
weights |
array | 항목별 가중치(number). data와 순서가 같습니다 |
addon |
string | 항상 빈 문자열 |
resultdata |
integer | 당첨 항목의 data 인덱스(0부터) |
{
"type": "run_roulette",
"roulette_name": "간식 룰렛",
"user_name": "시청자닉네임",
"data": ["치킨", "피자", "꽝"],
"weights": [20, 30, 50],
"addon": "",
"resultdata": 0
}
run_roulette2
룰렛 실행 한 번이 끝나면 그 실행의 결과를 모아 한 번 발행됩니다. 연차 보너스로 더 돈 회차도 들어갑니다. 대시보드 룰렛 테스트는 테스트 한 번마다 하나씩 옵니다.
| 필드 | 타입 | 설명 |
|---|---|---|
type |
string | run_roulette2 |
roulette_name |
string | 룰렛 이름 |
user_name |
string | 돌린 사람의 닉네임. run_roulette와 같습니다 |
data |
array | 룰렛 항목 이름 전체(string) |
addon |
string | 항상 빈 문자열 |
resultdata |
array | 당첨 항목의 data 인덱스(integer)를 회차 순서대로. 1회 실행이면 원소가 하나입니다 |
{
"type": "run_roulette2",
"roulette_name": "간식 룰렛",
"user_name": "시청자닉네임",
"data": ["치킨", "피자", "꽝"],
"addon": "",
"resultdata": [0, 2, 0]
}
대시보드의 룰렛 테스트와 홈 리모컨의 결과 다시 보기도 run_roulette·run_roulette2를 발행하며, 테스트인지 구분하는 필드는 없습니다. 다시 보기는 원래 기록의 닉네임을 user_name에 씁니다. 룰렛 이벤트에는 시청자 UID도 없으니 누가 돌렸는지 확인하려면 룰렛 내역을 조회하세요.
command
명령어를 실행했을 때 발행됩니다. 커스텀 명령어는 성공했을 때와, 쿨타임·유저별 쿨타임·개인 일일 제한·포인트 부족으로 실패했을 때(success: false) 옵니다. 그 밖의 실패(전체 일일 제한, 방송 중이 아님, 이미 출석함 등)에는 발행하지 않습니다. !sr 노래 신청(신청을 받는 중일 때)과 !문방구 뽑기는 성공과 실패 모두 발행합니다.
| 필드 | 타입 | 설명 |
|---|---|---|
type |
string | command |
user_chat |
string | 시청자가 친 채팅 원문 |
used_command |
string | 실행된 명령어. 커스텀 명령어는 등록된 이름(부분 일치 명령어는 %%가 붙은 이름), 노래 신청은 !sr, 문방구 뽑기는 !문방구 |
user |
object | 실행한 시청자 |
user.uid |
string | 시청자 UID |
user.nickname |
string | 시청자 닉네임 |
response |
string | 봇의 응답 문구. 실패면 실패 안내 문구입니다. 결과 채팅을 끈 룰렛에 연결된 명령어는 빈 문자열 |
success |
boolean | 실행에 성공했으면 true |
amount |
integer | 걸린 포인트. 커스텀 명령어는 명령어 포인트(연차면 곱한 값, 포인트를 주는 명령어는 음수), !sr은 SR 신청 포인트, !문방구는 성공했을 때의 뽑기 비용(실패면 0) |
optional_feature |
string | 명령어의 추가 기능 값. 출석 체크 명령어는 <출석체크>이고 보통은 빈 문자열입니다. 예전 잼쿠봇에서 옮겨 온 명령어는 다른 값일 수 있습니다 |
overlay_text |
string | 항상 빈 문자열 |
{
"type": "command",
"user_chat": "!간식 3",
"used_command": "!간식",
"user": {"uid": "fedcba9876543210fedcba9876543210", "nickname": "시청자닉네임"},
"response": "시청자닉네임님 치킨(2개), 꽝(1개) 당첨",
"success": true,
"amount": 300,
"optional_feature": "",
"overlay_text": ""
}
point_use
포인트가 0이 아닌 커스텀 명령어가 포인트를 뺀(또는 준) 직후, 그리고 !문방구 뽑기로 포인트를 썼을 때 발행됩니다. 포인트 설정은 커스텀 명령어를 참고하세요.
| 필드 | 타입 | 설명 |
|---|---|---|
type |
string | point_use |
user |
object | 시청자. uid, nickname은 command와 같습니다 |
amount |
integer | 뺀 포인트(양수). 포인트를 주는 명령어는 준 만큼의 음수 |
use_command |
string | 시청자가 친 채팅 원문 |
used_command |
string | 실행된 명령어 이름. !문방구 뽑기에는 이 필드가 없습니다 |
optional_feature |
string | command와 같은 값. !문방구 뽑기는 빈 문자열 |
overlay_text |
string | 빈 문자열. !문방구 뽑기는 x |
{
"type": "point_use",
"user": {"uid": "fedcba9876543210fedcba9876543210", "nickname": "시청자닉네임"},
"amount": 300,
"use_command": "!간식 3",
"used_command": "!간식",
"optional_feature": "",
"overlay_text": ""
}
attendance
출석 체크 명령어로 출석에 성공했을 때 발행됩니다. 같은 실행의 command 이벤트도 함께 옵니다.
| 필드 | 타입 | 설명 |
|---|---|---|
type |
string | attendance |
user |
object | 시청자. uid, nickname은 command와 같습니다 |
attendance_count |
string | 누적 출석 횟수. 숫자를 담은 문자열입니다 |
streak_count |
string | 연속 출석 횟수. 숫자를 담은 문자열입니다 |
last_attendance_date |
string | 이번 출석이 기록된 출석일, YYYY-MM-DD. 채널의 출석 기준 시각으로 나눈 날짜입니다 |
{
"type": "attendance",
"user": {"uid": "fedcba9876543210fedcba9876543210", "nickname": "시청자닉네임"},
"attendance_count": "12",
"streak_count": "3",
"last_attendance_date": "2026-10-05"
}
optional_feature
포인트가 0인 커스텀 명령어에 출석 체크가 아닌 추가 기능 값이 있을 때 발행됩니다. 지금 잼쿠봇에서는 이 값을 정할 수 없어서 예전 잼쿠봇에서 옮겨 온 명령어에서만 나옵니다.
| 필드 | 타입 | 설명 |
|---|---|---|
type |
string | optional_feature |
user |
object | 시청자. uid, nickname은 command와 같습니다 |
use_command |
string | 시청자가 친 채팅 원문 |
amount |
integer | 항상 0 |
optional_feature |
string | 명령어에 저장된 추가 기능 값 |
overlay_text |
string | 항상 빈 문자열 |
{
"type": "optional_feature",
"user": {"uid": "fedcba9876543210fedcba9876543210", "nickname": "시청자닉네임"},
"use_command": "!효과음",
"amount": 0,
"optional_feature": "효과음",
"overlay_text": ""
}
연결 유지와 종료
서버가 먼저 보내는 앱 수준의 하트비트는 없습니다. 연결과 권한이 살아 있는지 확인하려면 앱에서 주기적으로(예: 30초마다) {"type": "ping"}을 보내세요.
서버는 메시지를 보내거나 받을 때마다, 그리고 앱이 30초 동안 아무것도 보내지 않으면 그때마다 토큰·동의·권한·만료를 다시 확인합니다. 그 채널에 이벤트가 발행되면 이 연결이 받지 않는 타입이어도 확인합니다. 토큰이 더는 유효하지 않으면 연결을 4401로 닫으므로, 무효가 된 연결은 늦어도 30초 안에 닫힙니다. 예를 들면 다음과 같습니다.
- 액세스 토큰이 만료됨(발급 후 2시간)
- 토큰을 갱신함. 갱신하면 이전 토큰 쌍이 바로 무효가 됩니다
- 같은 채널로 다시 인가해 새 토큰을 받음
- 스트리머가 연결을 해제함 — 연결 해제
- 앱이 토큰 폐기를 호출했거나, 회전된 리프레시 토큰이 다시 쓰여 토큰 쌍이 폐기됨
- 다시 동의하거나 앱의 권한 선언을 줄여
events:subscribe가 빠짐 - 앱이 삭제됨
| 종료 코드 | 뜻 | 앱이 할 일 |
|---|---|---|
4401 |
토큰이 없거나 더는 유효하지 않음, 또는 events:subscribe 권한 없음 |
토큰을 갱신하고 새 액세스 토큰으로 다시 연결합니다. 갱신이 invalid_grant로 실패하거나 갱신 응답의 scope에 events:subscribe가 없으면 스트리머에게 다시 동의를 받습니다 |
4401 |
앱이 보낸 메시지 하나가 64KiB를 넘었거나, 같은 IP에서 1분에 240개 넘게 보냄 | ping 외에는 보낼 메시지가 없으니 주기를 늘리고 다시 연결합니다 |
4401 |
서버가 이벤트를 5초 안에 전달하지 못함 | 수신 처리를 막지 않게 고친 뒤 다시 연결합니다 |
토큰을 갱신하면 열려 있던 연결은 늦어도 30초 안에 닫히므로, 갱신한 직후 새 토큰으로 다시 연결하세요. 갱신 방법은 토큰 갱신에 있습니다.
끊긴 동안 생긴 이벤트는 다시 보내지 않고, 끊긴 지점부터 이어 받는 기능도 없습니다. 놓친 룰렛 결과는 룰렛 내역으로 확인할 수 있습니다.
이 가이드로 해결이 안 됐나요?
디스코드로 문의하기