공통 사항
기본 주소, 인증 헤더, 응답 형식, 페이지네이션, 오류와 제한 등 모든 API에 공통인 규칙입니다.
목차
모든 개발자 API에 똑같이 적용되는 주소, 인증, 요청·응답 형식, 오류와 요청 한도를 설명합니다. API별 문서는 이 규칙을 바탕으로 합니다.
기본 주소
REST API와 WebSocket은 모두 아래 기본 주소 아래에 있습니다. API 버전은 경로의 v1입니다.
https://jamkubot.com/api/v1/developer
이 문서의 요청 예시는 호스트를 빼고 /api/v1/developer/commands처럼 경로만 적습니다. 실제로는 앞에 https://jamkubot.com을 붙여 HTTPS로 호출합니다. 실시간 이벤트는 같은 기본 주소의 /ws에 wss://로 연결합니다(WebSocket 이벤트).
OAuth 인가와 토큰 발급은 기본 주소 밖에 따로 있습니다. 사용법은 OAuth 인가 흐름을 보세요.
| 용도 | 주소 |
|---|---|
| 스트리머 동의 화면 | https://jamkubot.com/developers/oauth/authorize |
| 토큰 발급·갱신 | https://jamkubot.com/developers/oauth/token |
| 토큰 폐기 | https://jamkubot.com/developers/oauth/revoke |
API별 경로와 필요한 권한은 권한별 API에 모여 있고, 바뀐 점은 변경 이력에서 확인합니다.
인증
모든 REST 요청은 OAuth로 받은 액세스 토큰을 Authorization 헤더에 담아 보냅니다. 예시의 ACCESS_TOKEN을 실제 토큰으로 바꾸세요.
GET /api/v1/developer/commands
Authorization: Bearer ACCESS_TOKEN
Bearer와 토큰 사이는 공백으로 띄웁니다.Bearer는 대소문자를 가리지 않아bearer도 됩니다.- REST API는
?access_token=쿼리를 읽지 않습니다. 헤더 없이 쿼리로만 보내면 401입니다. 쿼리로 토큰을 받는 곳은 WebSocket 연결뿐입니다. - 토큰 하나는 앱과 스트리머 채널 한 쌍에 발급되고 그 채널의 데이터만 다룹니다. 주소에 채널 UID를 넣지 않습니다. 여러 채널을 다루려면 채널마다 동의를 받아 토큰을 따로 보관하세요. 토큰이 어느 채널의 것인지는 토큰 정보 (me)로 확인합니다.
- 서버는 요청마다 토큰, 스트리머의 지금 동의, 앱이 선언한 권한을 다시 확인하고 셋에 모두 있는 권한만 허용합니다. 스트리머가 연결을 해제하거나 권한이 줄면 다음 요청부터 바로 반영됩니다(권한 변경).
액세스 토큰은 발급 후 2시간이 지나면 만료됩니다. 토큰 갱신으로 새 토큰을 받으세요.
요청 형식
- 본문이 있는 요청(
POST,PUT)은 JSON으로 보내고Content-Type: application/json헤더를 붙입니다. 다른Content-Type으로 보내면 본문을 객체로 읽지 못해 422가 됩니다. - 조회 조건은 쿼리 문자열로 보냅니다.
- 경로 변수와 쿼리 값은 UTF-8로 URL 인코딩합니다. 한글이나 공백이 든 룰렛 이름도 인코딩해서 넣습니다.
- 본문에 정의되지 않은 필드는 오류 없이 무시됩니다. 값이 반영되지 않으면 필드 이름의 철자부터 확인하세요.
간식 룰렛을 조회하는 예시입니다.
GET /api/v1/developer/roulettes/%EA%B0%84%EC%8B%9D%20%EB%A3%B0%EB%A0%9B
Authorization: Bearer ACCESS_TOKEN
본문 크기와 JSON 구조의 한도는 본문 크기를 보세요.
응답 형식
성공 응답은 감싸지 않은 JSON입니다. {"code": …, "content": …} 같은 공통 봉투가 없으니 본문을 바로 읽습니다. 필드 이름은 snake_case입니다.
| 요청 | 상태 | 본문 |
|---|---|---|
| 단건 조회 | 200 |
객체 |
| 목록 조회 | 200 |
배열. 포인트 목록과 룰렛 내역은 페이지 객체 |
생성 (POST) |
200 |
만든 객체 |
수정 (PUT) |
200 |
수정한 뒤의 객체 |
삭제 (DELETE) |
200 |
{"ok": true} |
생성도 201이 아니라 200이고, 삭제도 204 대신 본문을 돌려줍니다. SR 재생 제어는 {"ok": true, "state": {...}}처럼 바뀐 상태를 함께 줍니다.
값의 타입은 다음을 주의하세요.
- 값이 없으면
null입니다. - 켜짐·꺼짐 필드는
true/false입니다. - 명령어의
point는 절댓값이 9,007,199,254,740,991(2^53-1)을 넘으면 정확한 값을 지키려고 문자열로 옵니다. 그 밖의 정수는 크기와 상관없이 JSON 숫자입니다.
응답에는 필드가 늘어날 수 있으니 모르는 필드는 무시하도록 만드세요.
페이지네이션
페이지로 나눠 주는 API는 포인트 목록과 룰렛 내역 둘입니다. 명령어·룰렛 목록은 페이지 없이 전체를 한 번에 줍니다.
요청은 쿼리 page와 size로 합니다.
| 쿼리 | 범위 | 기본값 |
|---|---|---|
page |
1 이상. 1부터 셉니다 | 1 |
size |
1~200 | 포인트 목록 50, 룰렛 내역 20 |
범위를 벗어나면 422입니다. 응답은 둘 다 아래 필드를 가진 페이지 객체입니다.
| 필드 | 타입 | 설명 |
|---|---|---|
page |
integer | 요청한 페이지 |
size |
integer | 한 페이지의 최대 항목 수 |
total |
integer | 전체 항목 수 |
total_page |
integer | 전체 페이지 수. 항목이 없으면 0 |
has_next |
boolean | 다음 페이지가 있으면 true |
has_prev |
boolean | page가 2 이상이면 true |
data |
array | 이 페이지의 항목. 범위 밖 페이지면 [] |
룰렛 내역에는 has_next와 같은 값인 next도 들어 있습니다. 전체를 읽으려면 page=1부터 has_next가 false가 될 때까지 page를 1씩 올립니다.
오류
REST API는 실패하면 HTTP 상태 코드와 함께 detail 필드를 줍니다. detail은 사람이 읽는 설명이고 기계용 오류 코드는 없으니, 처리는 상태 코드로 나누세요.
{"detail": "유효하지 않거나 만료된 액세스 토큰입니다"}
| 상태 | 응답 | 원인 |
|---|---|---|
| 400 | {"detail": "이미 존재하는 명령어입니다."} 등 |
형식은 맞지만 처리할 수 없는 요청입니다. 이름 중복, 잘못된 룰렛 확률, SR의 잘못된 action 등이며 문구는 API마다 다릅니다. JSON 구조 한도를 넘어도 400입니다 |
| 401 | {"detail": "유효하지 않거나 만료된 액세스 토큰입니다"} |
헤더가 없거나 Bearer 형식이 아님, 토큰 만료, 갱신으로 무효가 된 이전 토큰, 스트리머의 연결 해제, 토큰·동의·앱 선언에 공통으로 남은 권한이 없음 등 |
| 403 | {"detail": "이 작업에는 'commands:write' 권한(scope)이 필요합니다"} |
토큰은 유효하지만 이 API에 필요한 권한이 없습니다. 작은따옴표 안이 필요한 권한입니다 |
| 404 | {"detail": "명령어가 없습니다"} 등 |
대상이 없습니다. 없는 경로는 {"detail": "Not Found"}입니다 |
| 405 | {"detail": "Method Not Allowed"} |
그 경로가 받지 않는 메서드입니다 |
| 413 | {"detail": "Request body too large"} |
본문이 10MB를 넘었습니다 |
| 422 | {"detail": [...]} |
필수 값이 없거나 타입·길이·범위가 맞지 않습니다 |
| 429 | {"detail": "요청이 너무 잦습니다. 12초 후 다시 시도해 주세요."} 등 |
요청 횟수 한도를 넘었습니다. Retry-After 헤더가 함께 옵니다 |
| 500 | {"detail": "Internal Server Error"} |
서버에서 처리하지 못했습니다 |
422의 detail은 문제 하나마다 객체 하나인 배열입니다. loc이 위치(body, query, path와 필드 이름), msg가 영어 설명입니다.
{
"detail": [
{
"type": "missing",
"loc": ["body", "response"],
"msg": "Field required",
"input": {"command": "!인사"}
}
]
}
401과 403에는 WWW-Authenticate 헤더도 붙습니다. 토큰이 무효면 Bearer realm="jamkubot", error="invalid_token"(토큰을 아예 보내지 않았으면 error 없이 Bearer realm="jamkubot"), 권한이 모자라면 Bearer realm="jamkubot", error="insufficient_scope", scope="commands:write"처럼 필요한 권한이 함께 옵니다.
상태별로 이렇게 대응합니다.
- 401: 토큰을 갱신하고 다시 요청합니다. 갱신도 실패하면 스트리머에게 다시 동의를 받아야 합니다.
- 403: 권한별 API에서 필요한 권한을 확인합니다. 스트리머가 그 권한에 동의하지 않았거나, 앱 설정에서 권한을 빼서 발급된 토큰에서도 빠졌을 수 있습니다. 새 권한은 스트리머에게 다시 동의를 받아야 생깁니다.
- 429:
Retry-After헤더의 초만큼 기다렸다가 다시 보냅니다.
OAuth 주소(/developers/oauth/*)의 오류는 {"error": "...", "error_description": "..."} 형식입니다. OAuth 오류를 보세요. WebSocket은 HTTP 상태 대신 종료 코드로 알립니다(WebSocket 연결).
제한
요청 횟수
| 대상 | 한도 | 넘었을 때 |
|---|---|---|
개발자 API /api/v1/developer/* |
앱(client_id) 하나당 분당 30회. 그 앱이 연결된 모든 채널의 호출을 합쳐 셉니다 |
429 {"detail": "요청이 너무 잦습니다. …초 후 다시 시도해 주세요."}, Retry-After(남은 초) |
| 한 채널에 대한 개발자 API 호출 | 그 채널에 연결된 모든 앱을 합쳐 분당 900회 | 429 {"detail": "앱 요청이 너무 잦습니다. 잠시 후 다시 시도해 주세요."}, Retry-After: 60 |
| 같은 IP 주소의 API·OAuth 요청 | 모든 경로를 합쳐 분당 1,200회 | 429 {"detail": "요청이 너무 잦습니다. 잠시 후 다시 시도해 주세요."}, Retry-After: 60 |
| 개발자 API를 포함한 모든 HTTP 요청 | 같은 IP 주소에서 같은 경로로 분당 600회 | 429, Retry-After: 60 |
토큰 발급·갱신·폐기 /developers/oauth/token, /developers/oauth/revoke |
같은 IP 주소·같은 앱(client_id)의 요청 분당 15회. 앱 인증을 통과한 발급·갱신은 IP와 상관없이 앱 하나당 분당 15회 |
429 slow_down, Retry-After: 60 |
앱 하나가 가장 먼저 닿는 한도는 앱 단위 한도(분당 30회)입니다. 토큰과 권한 확인을 통과한 요청만 세고(401·403 은 세지 않습니다), WebSocket 연결과 이벤트는 세지 않습니다. 한도를 세는 구간은 1분 단위로 고정돼 있고, 응답마다 아래 헤더로 남은 횟수를 알려 줍니다.
| 헤더 | 설명 |
|---|---|
X-RateLimit-Limit |
구간당 최대 요청 수(30) |
X-RateLimit-Remaining |
이번 구간에 남은 요청 수 |
X-RateLimit-Reset |
구간이 초기화되는 시각(Unix epoch 초) |
HTTP/1.1 200 OK
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 12
X-RateLimit-Reset: 1791234600
- 앱 단위 한도로 받은 429 에도 같은 헤더와
Retry-After(초기화까지 남은 초)가 붙습니다. 다른 한도의 429 에는X-RateLimit-*없이Retry-After만 오니, 429 는 언제나Retry-After만큼 기다렸다가 다시 보냅니다. - 상태를 자주 확인해야 하면 짧은 주기로 폴링하지 말고 WebSocket 이벤트를 쓰세요. 앱이 여러 채널에 연결돼 있으면 채널 수만큼 폴링 간격을 늘려야 30회 안에 들어옵니다.
- 앱의 하루 호출 수는 앱 상세의 사용 현황에서 볼 수 있습니다.
전역 한도의 경로는 쿼리를 뺀 주소로 셉니다. /api/v1/developer/points?page=1과 ?page=2는 같은 경로로 함께 세고, /roulettes/A와 /roulettes/B는 따로 셉니다. 메서드는 구분하지 않습니다. 한도를 넘으면 이런 응답이 옵니다.
{
"error": "Rate limit exceeded: 600 per 1 minute",
"detail": "Rate limit exceeded: 600 per 1 minute"
}
전역 한도의 Retry-After는 한도를 세는 구간의 길이(60초)입니다. 그만큼 기다리면 다시 요청할 수 있습니다.
토큰·폐기 엔드포인트는 두 가지를 셉니다. 같은 IP 주소에서 같은 앱(client_id)으로 보낸 요청은 인증 성공·실패와 상관없이 분당 15회까지 받고, 앱 인증을 통과한 발급·갱신은 IP와 상관없이 앱 하나당 분당 15회까지 받습니다. 넘으면 429 {"error": "slow_down", "error_description": "요청이 너무 잦습니다. 60초 후 다시 시도해 주세요."}(앱 발급 한도는 "앱의 토큰 발급 한도를 초과했습니다")와 Retry-After: 60이 옵니다. client_id는 대소문자를 가리지 않고 셉니다. 다른 사람이 client_id만 알고 보내는 요청은 그 사람의 IP 몫만 쓰므로 앱 서버의 발급을 막지 못합니다. 토큰은 만료될 때까지 보관해 다시 쓰고, 여러 채널의 토큰을 한꺼번에 갱신해야 하면 분당 15회 안으로 나눠 보내세요.
액세스 토큰(2시간), 인가 코드(5분, 1회용) 같은 유효기간은 유효기간에 있습니다.
본문 크기
| 항목 | 한도 | 넘었을 때 |
|---|---|---|
| 본문 크기 | 10MB (10,485,760바이트) | 413 {"detail": "Request body too large"} |
| JSON 중첩 깊이 | 20단계 | 400 {"detail": "JSON nesting depth exceeds 20"} |
| JSON 키 개수 | 1,000개 (본문 안 모든 객체의 키를 합친 수) | 400 {"detail": "JSON keys exceed 1000"} |
JSON 한도는 Content-Type이 없거나 application/json(+json 형식 포함)인 요청에 적용됩니다.
이 가이드로 해결이 안 됐나요?
디스코드로 문의하기