Getting Started
Quickstart
Run the server and read a live chat, first with curl and then with the client library.
Prerequisites
- Bun — the API server's runtime.
- pnpm 10 — the workspace's package manager.
- A YouTube video that has live chat: either currently live, or a finished stream whose replay chat is still available.
Run the server
pnpm install
pnpm --filter @yt-livechat/api devThe server listens on port 3000 and mounts everything under /v1. CORS
headers are sent on every response, so a browser page may call it directly.
The api's
devandtypecheckscripts build the client package first, because the server imports its shared schemas from that package'sdist/.
With no environment set, the server keeps its message archive in memory —
fine for trying it out, but a restart discards the history. Set PB_URL (plus
PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD) to persist it.
Read a chat with curl
One request. Leave it open and messages arrive as they happen.
curl -N -H 'Accept: text/event-stream' \
'http://localhost:3000/v1/stream?videoId=zvwJ29RFVww&backfill=5'-N disables curl's output buffering; without it you will see nothing until
the connection closes.
A full YouTube URL works too, but percent-encode it — an unescaped one carries
its own ? and & into the query string:
curl -N -G -H 'Accept: text/event-stream' \
--data-urlencode 'videoId=https://www.youtube.com/watch?v=zvwJ29RFVww' \
http://localhost:3000/v1/streamretry: 3000
event: init
data: {"videoId":"zvwJ29RFVww","chatType":"liveChatRenderer","cursor":4211}
event: message
id: 4212
data: {"type":"chat","id":"ChwKGkNJ...","timestamp":"1:23 PM","author":"Some Viewer","message":"hello :yt:","amount":"","color":"","parts":[{"kind":"text","text":"hello "},{"kind":"emoji","shortcut":":yt:","emoji_id":"yt","is_custom":false,"mapped_unicode":"▶️","thumbnails":[]}]}
: keepalive
event: message
id: 4213
data: { ... }That is the whole protocol. videoId accepts a full YouTube URL or a bare
11-character id; backfill (default 50) is how much history to start with.
The id: on each message is a cursor. If the connection drops, send the
last one back as Last-Event-ID and the server resumes from exactly there:
curl -N -H 'Accept: text/event-stream' -H 'Last-Event-ID: 4213' \
'http://localhost:3000/v1/stream?videoId=zvwJ29RFVww'The stream ends with an end frame when the chat is over:
event: end
data: {"reason":"no more continuations"}See GET /v1/stream for every frame and field.
Read a chat with the client library
Reconnection, cursor tracking, rate-limit backoff and message pacing are what the client package does for you:
npm install @gettersethya/yt-livechat-clientpnpm add @gettersethya/yt-livechat-clientyarn add @gettersethya/yt-livechat-clientbun add @gettersethya/yt-livechat-clientimport { LiveChatApiClient } from '@gettersethya/yt-livechat-client'
const client = new LiveChatApiClient({
baseUrl: 'http://localhost:3000',
videoUrl: 'https://www.youtube.com/watch?v=zvwJ29RFVww',
})
client.on('connected', () => console.log('connected:', client.videoId))
client.on('message', (message) => console.log(`${message.author}: ${message.message}`))
client.on('error', (error) => console.error(error.code, error.message))
client.on('end', (reason) => console.log('ended:', reason))
await client.connect()
await client.start()start() resolves when the stream ends. Call client.stop() from anywhere to
break out; it returns immediately and never blocks.
Using React, Vue or Svelte? Framework Bindings
gives you a useLiveChat hook that owns this lifecycle.
Check which clients work for a video
If a video returns no messages, ask the server to try every innertube client and endpoint combination and report what worked:
curl -s http://localhost:3000/v1/probe \
-H 'Content-Type: application/json' \
-d '{"videoId":"zvwJ29RFVww"}'See Probe for how to read the result.