Search documentation

Search pages and headings

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 dev

The 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 dev and typecheck scripts build the client package first, because the server imports its shared schemas from that package's dist/.

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/stream
retry: 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-client
pnpm add @gettersethya/yt-livechat-client
yarn add @gettersethya/yt-livechat-client
bun add @gettersethya/yt-livechat-client
import { 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.