Getting Started
Introduction
A streaming HTTP API that turns YouTube live chat — live and replay — into normalized JSON over SSE.
yt-livechat reads YouTube live chat through YouTube's own internal
("innertube") endpoints and re-publishes it as plain JSON over HTTP. It works
for both an ongoing live stream and the replay of a finished one, and it needs
no Google API key and no OAuth.
Why it exists
The official YouTube Data API charges quota for live chat polling and stops working entirely once a broadcast ends. The innertube endpoints that the YouTube web player itself uses have neither limitation, but talking to them requires reproducing a browser's bootstrap: scraping an API key, matching a client version, brace-matching a JSON blob out of an HTML page, and then carrying an opaque continuation token forward on every request.
This project does that once, on the server, and hands you a stable contract.
The shape of the system
There are two published pieces.
| Package | What it is |
|---|---|
apps/api |
The HTTP server. Bun runtime, effect/unstable/http router. Exposes /v1. |
@gettersethya/yt-livechat-client |
A TypeScript client that consumes the stream for you. |
The client never talks to YouTube — it only talks to your API server.
The server owns the polling loop
This is the single most important property to understand, because it shapes the whole API.
The server runs one poller per video, no matter how many people are watching, and fans it out to every listener over Server-Sent Events:
GET /v1/stream?videoId=...
|
v
event: init -> videoId, chatType, your starting cursor
event: message -> one per message, each with an id: cursor
event: message
...
event: end -> the chat is overThe practical consequences:
- Viewers are cheap. A thousand viewers of one stream cost the same upstream traffic as one. Your load on YouTube scales with distinct videos, not with users.
- Nothing sensitive reaches the client. The innertube API key, the client version and the continuation token stay on the server.
- Reconnects are gapless. Every message carries a cursor; send back the
last one you saw and the server resumes exactly there. Browsers do this
automatically with
Last-Event-ID. - You do not manage a loop, a token or a poll interval. Open the stream and read it.
The trade-off is that the server is now stateful: it keeps a poller and a message archive per video, so run a single instance. Two would each poll the same video and hand out conflicting cursors.
What you get back
Every chat event is normalized into one flat Message shape regardless of
which YouTube renderer produced it — a plain message, a Super Chat, a sticker,
a membership, a gift, or a deletion all arrive with the same fields. Message
bodies come in two parallel forms: message, a flattened plain-text string,
and parts, a structured array that preserves custom emoji so you can render
them as images.
See Message Schema for the field list and
Message Types for the full set of type
values.
Where to go next
- Quickstart — get a stream flowing in a couple of minutes.
- Stream Reference — the frame-by-frame contract.
- Client Usage — let the client library handle reconnects and pacing.