Build a bot
Follow this guide to build a bot that receives a DJ's live chat in real time and sends messages under the bot's own name. It runs from getting consent all the way to the bot's first message.
What you need
- The Client ID and Client Secret from Register an app
- The login redirect URL registered on your app
- The permissions (scopes) registered on your app — this guide uses
events.chatandchat.send - Node.js 22.6 or later — the examples are TypeScript, so
--experimental-strip-typesis required
API address
The examples below write the base URL as $SPOON_BASE_URL. Set it to the address of the
region you are integrating with.
export SPOON_BASE_URL="https://kr-openapi.spooncast.net"
| Region | Base URL |
|---|---|
| Korea | https://kr-openapi.spooncast.net |
| Japan | https://jp-openapi.spooncast.net |
Use the address of the region your app is registered in. The DJ's account decides the region, so calling another region's address will not find that DJ's broadcast.
1. Get the DJ's consent
Send the DJ's browser to the consent screen and receive the code that comes back.
The full rules are in Get user consent.
https://developers.spooncast.net/kr/oauth/authorize
?response_type=code
&client_id={Client ID}
&redirect_uri={login redirect URL}
&scope=events.chat%20chat.send
&state={a random value you generate}
The authorize URL carries the region too. Korea uses /kr, Japan uses /jp.
If the DJ approves, a code is appended to your redirect URL. You must use it
within 60 seconds.
2. Exchange the code for a token
Exchange the code for an access token. This call uses your Client Secret, so make
it from your server only.
curl -X POST "$SPOON_BASE_URL/v1/oauth/token" \
-u "{Client ID}:{Client Secret}" \
-d "grant_type=authorization_code" \
-d "code={the code you received}" \
-d "redirect_uri={login redirect URL}"
{
"access_token": "at_3f9c1e7b8d2a4056...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_7b2e5a9c4f10d386...",
"scope": "events.chat chat.send"
}
| Value | Lifetime | What to know |
|---|---|---|
access_token | 1 hour | Attach it to every bot API call as Authorization: Bearer. |
refresh_token | 30 days | Use it to get a new access token. |
A long-running bot must implement refresh. When the access token expires, any open event stream ends with it. Refresh well ahead of expiry (an hour before, for example), and persist the result so it survives a restart. If you keep it only in memory, the DJ has to grant consent again every time your process restarts.
Refreshing also replaces the refresh token. If you do not store the new
refresh_token from the response, your next refresh fails — see
Tokens for the details.
Refresh through the same endpoint with grant_type=refresh_token.
curl -X POST "$SPOON_BASE_URL/v1/oauth/token" \
-u "{Client ID}:{Client Secret}" \
-d "grant_type=refresh_token" \
-d "refresh_token={the refresh_token you received}"
Export the token you received as well.
export SPOON_ACCESS_TOKEN="{access_token}"
3. Connect to the event stream
The moment you connect, a listener notice is posted to the broadcast under your bot's name. There is nothing to do in your code — see Listener notice.
If the DJ is on air, chat starts flowing immediately. Check it with curl first.
curl -N \
-H "Authorization: Bearer {access_token}" \
-H "Accept: text/event-stream" \
"$SPOON_BASE_URL/v1/live/events"
:heartbeat
id:1788328046114-0
event:chat
data:{"user":{"id":"Ab1Cd2Ef3Gh4Ij5Kl6Mn7O","nickname":"SpoonListener"},"isDj":false,"message":"Hello","sentTime":"2026-09-02T05:47:26.114Z"}
If the DJ is not on air you get a 404. That is a state, not an error — wait until
the DJ goes live and connect again.
Here is the same thing in Node. Save it and run it, and chat is printed to the console.
// bot.ts — node --experimental-strip-types bot.ts
interface EventUser {
id: string;
nickname: string | null;
}
interface ChatEvent {
user: EventUser;
isDj: boolean;
message: string;
sentTime: string;
}
const BASE = process.env.SPOON_BASE_URL;
const TOKEN = process.env.SPOON_ACCESS_TOKEN;
const res = await fetch(`${BASE}/v1/live/events`, {
headers: { Authorization: `Bearer ${TOKEN}`, Accept: 'text/event-stream' },
});
if (!res.ok || !res.body) throw new Error(`Could not open the stream: ${res.status}`);
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
// SSE separates events with a blank line; the last chunk is incomplete, so keep it buffered
const frames = buffer.split('\n\n');
buffer = frames.pop() ?? '';
for (const frame of frames) {
const lines = frame.split('\n');
const event = lines.find((l) => l.startsWith('event:'))?.slice(6);
const data = lines.find((l) => l.startsWith('data:'))?.slice(5);
if (!data) continue; // frames without data, such as `:heartbeat`
if (event === 'chat') {
const chat = JSON.parse(data) as ChatEvent;
console.log(`${chat.user.nickname ?? 'Anonymous'}: ${chat.message}`);
}
}
}
node --experimental-strip-types bot.ts
The browser EventSource cannot connect. The token goes in the Authorization header, and EventSource cannot set headers.
Run the code above on your server and relay the result to the browser.
4. Send a message
Send chat under the bot account's name. On success you get a 204 with no body.
curl -i -X POST "$SPOON_BASE_URL/v1/live/chat" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"message":"Hello! Your bot says hi"}'
HTTP/2 204
How to check it worked
While the DJ is on air, your bot is wired up correctly if all of the following hold.
- The stream opens with
200and a:heartbeatline arrives every 15 seconds. - When a listener types in chat, an
event: chatframe arrives right away. POST /v1/live/chatreturns204, and the message appears in the Spoon app chat under the bot account's name.- Messages your bot sends do not come back on your own stream — that rule prevents echo loops.
Next steps
- Tokens — the full spec for issuing, refreshing, and revoking.
- API reference — request, response, and examples for each endpoint.
- Broadcast read permissions — the current broadcast, listeners, and fan ranking.
- Event stream — event types, payload schemas, and reconnection strategy.
- Send chat — length limits and failure handling.
- Choosing permissions — what each permission grants.
- Error codes — how to react to each response.