youtube-chat-next
v3.1.0
Published
Fetch YouTube Live chat. Maintained fork of youtube-chat.
Downloads
304
Maintainers
Readme
youtube-chat-next
Read any public YouTube live chat with no API key, no OAuth, no Google Cloud project. Just a video, channel, or handle. Unofficial continuation of youtube-chat by LinaTsukusu, now maintained for modern YouTube.
Original repository: https://github.com/LinaTsukusu/youtube-chat
This fork: https://github.com/LucasSantana-Dev/youtube-chat-next
License: MIT (original copyright preserved)
No API key required, that's the point
The reason to reach for this library is that it needs zero credentials:
- No API key. You never register anything. The key it uses is YouTube's own public web key, scraped from the watch page at runtime, an internal detail you never see or supply.
- No OAuth, no consent screen, no token refresh.
- No Google Cloud project and no quota. The official YouTube Data API v3 meters
liveChatMessages.listagainst a daily quota; this has none. - No per-stream authorization. You can read the chat of any public live stream, not only ones you own or moderate.
import { LiveChat } from "youtube-chat-next"
// This is the whole setup. No keys, no config, no accounts.
const chat = new LiveChat({ handle: "@LofiGirl" })
chat.on("chat", (item) => console.log(item.author.name))
await chat.start()That capability is the load-bearing design goal of this fork. It is preserved deliberately, and the daily drift canary exists to catch the day YouTube changes something that would break it.
The tradeoff, stated honestly
Needing no key is exactly because this is not an official, sanctioned path, see below.
How this works & what that means
This library scrapes YouTube's private InnerTube endpoint (/youtubei/v1/live_chat/get_live_chat) by extracting YouTube's own public web API key and a continuation token from the watch page. You never provide the key; it is read from the page for you. It is not the official YouTube Data API. It is not affiliated with, endorsed by, or supported by YouTube or Google.
Reality check
- It can break at any time without notice. YouTube's client is constantly changing. This library has survived 3.5 years unmaintained (v2.2.0, Dec 2022 → now), and it still works on today's YouTube. But that is not a guarantee for next month or next year.
- Automated access may violate YouTube's Terms of Service. You are responsible for your own use. In practice, enforcement is technical (rate-limiting, IP blocks) rather than legal. Decide for yourself whether this fits your use case.
- The official alternative exists. The YouTube Data API v3's
liveChatMessages.listis compliant and stable. Real tradeoff: it requires a Google Cloud project, an API key, and OAuth, is capped by a daily quota, and can realistically only read chat for streams you own or moderate, not arbitrary public streams. This library needs none of that and reads any public stream, but carries the risks above. Pick the official API when compliance and stability matter more than zero-setup and open access; pick this when they don't.
Best practices
- Always attach an
errorlistener. Node's EventEmitter throws if anerrorevent fires with no listener, which will crash your process. - Datacenter and cloud IPs are rate-limited more aggressively than residential ones. If you deploy on AWS, Google Cloud, or DigitalOcean and hit limits, honour the
RateLimitErrorand itsRetry-After, lower your request volume (a largerinterval), and stop after bounded retries rather than hammering through the limit.
What changed in this fork
Verified against live YouTube on 2026-07-15:
Respects YouTube's polling rate. YouTube's response includes
timeoutMs(typically 10000ms) saying how often to poll. Upstream polled every 1000ms, ignoring this, about 10× more requests than YouTube invites. Nowintervalis a floor; the library honoursmax(interval, timeoutMs). Measured: 71 messages delivered, 4 requests instead of ~33.No longer mutates the global axios. Upstream set
axios.defaults.headers.common["Accept-Encoding"] = "utf-8", changing the singleton for your entire application. Now uses a private axios instance.Detects stream end. When a stream ends,
continuationContentsdisappears from YouTube's response. Upstream crashed withTypeError, swallowed it, and retried forever. Now emits anendevent cleanly.No overlapping requests. Replaced
setIntervalwith a self-scheduling fetch loop. If a request takes longer than the polling interval, upstream would fire overlapping requests. Now each fetch waits for the previous one to complete.Typed errors. Four new error classes let you distinguish "YouTube changed the page shape" from "rate limited" from "stream finished":
ScrapeError, parsing failed (YouTube likely changed the HTML/JSON structure)NotLiveError, channel is not currently liveRateLimitError, 429 or 403 from YouTube (back off and retry)ParseError, response is malformed
Backoff with jitter on rate limits. On 429/403, honours the
Retry-Afterheader, adds random jitter, and gives up after 5 consecutive failures instead of looping forever.Locale pinned via Accept-Language. YouTube localises the watch page by IP address, so parsing previously depended on where your process ran. Now forces
Accept-Language: en-USso parsing is stable.axios upgraded 1.2.0 → 1.18.1. Axios 1.2.0 did not properly decompress YouTube's gzip/brotli responses. Upgrade is required for correctness.
Live contract test + daily CI canary. The test suite runs on frozen 2022 YouTube fixtures (cannot detect drift). Now
npm run test:liveruns against live YouTube daily to catch regressions early.
Getting started
1. Install
npm install youtube-chat-nextor
yarn add youtube-chat-next2. Import
JavaScript
const { LiveChat } = require("youtube-chat-next")TypeScript
import { LiveChat } from "youtube-chat-next"3. Create an instance
// Option A: Pass channelId (recommended)
// The library will fetch the current liveId automatically.
const liveChat = new LiveChat({ channelId: "CHANNEL_ID_HERE" })
// Option B: Pass liveId directly
// Use this if you already know the live stream ID.
const liveChat = new LiveChat({ liveId: "LIVE_ID_HERE" })
// Option C: Pass a YouTube handle (starts with @)
const liveChat = new LiveChat({ handle: "@channel_name" })
// Optional: Set the polling interval (milliseconds)
// Default is 1000. YouTube typically suggests 10000.
// The library will honour whichever is greater.
const liveChat = new LiveChat(
{ channelId: "CHANNEL_ID" },
5000 // interval in milliseconds
)
// Optional: Choose which chat view to read.
// "top" (default), YouTube's "Top chat": an algorithmically FILTERED subset.
// "live", "Live chat": every message.
// The default is "top" to match YouTube's own default; pass "live" for the full stream.
const liveChat = new LiveChat({ channelId: "CHANNEL_ID" }, 1000, "live")Note: by default this library reads "Top chat", exactly as YouTube's watch page does, a filtered subset, not every message. This was verified by measurement, not assumed. Pass
"live"as the third argument to read the complete unfiltered chat.
4. Add event listeners
// Fires when the fetch loop starts successfully.
// liveId: string
liveChat.on("start", (liveId) => {
console.log("Live chat started for stream:", liveId)
})
// Fires when a message arrives.
// chat: ChatItem (see Types section below)
liveChat.on("chat", (chatItem) => {
// message is a MessageItem[], each item is either a text run or an emoji.
const text = chatItem.message.map((m) => ("text" in m ? m.text : m.emojiText)).join("")
console.log(`${chatItem.author.name}: ${text}`)
})
// Fires when the stream ends or an unrecoverable error occurs.
// reason: string | undefined
liveChat.on("end", (reason) => {
console.log("Stream ended:", reason)
})
// Fires on errors. REQUIRED, never omit this.
// Distinguishing errors helps you respond correctly:
// - ScrapeError/ParseError: YouTube changed; alert and stop.
// - RateLimitError: Back off and retry.
// - NotLiveError: No stream currently live.
liveChat.on("error", (err) => {
if (err instanceof RateLimitError) {
console.error("Rate limited. Backing off...")
} else if (err instanceof ScrapeError) {
console.error("YouTube structure changed. Stopping.")
} else {
console.error("Error:", err)
}
})5. Start the fetch loop
// start() resolves to true if the stream is live and fetching,
// or false if setup failed (check the emitted error).
const ok = await liveChat.start()
if (!ok) {
console.log("Failed to start. See error event for details.")
}6. Stop the loop
liveChat.stop("Stream ended by caller")Types
ChatItem
A single message from the live chat.
interface ChatItem {
id: string // Unique message ID
author: {
name: string
thumbnail?: ImageItem // User's avatar
channelId: string
badge?: { // e.g. "Moderator", "New member"
thumbnail: ImageItem
label: string
}
}
message: MessageItem[] // Array of text + emoji
superchat?: { // Paid message (YouTube Super Chat)
amount: string // e.g. "US$1.00"
color: string // Hex color
sticker?: ImageItem // Animated sticker
}
isMembership: boolean // Is channel member?
isVerified: boolean // Verified account?
isOwner: boolean // Stream owner?
isModerator: boolean // Moderator?
timestamp: Date // Message time
}MessageItem
A single text or emoji element within a message.
type MessageItem = { text: string } | EmojiItemImageItem
A reference to an image (thumbnail, emoji, sticker).
interface ImageItem {
url: string // Full URL
alt: string // Alt text / emoji name
}EmojiItem
An emoji or custom emote.
interface EmojiItem extends ImageItem {
emojiText: string // Emoji as text (e.g., "😀") or custom name
isCustomEmoji: boolean // Is this a channel custom emote?
}Error types
import { LiveChat, ScrapeError, NotLiveError, RateLimitError, ParseError } from "youtube-chat-next"
// All inherit from Error and include:
// - message: string
// - name: "ScrapeError" | "NotLiveError" | "RateLimitError" | "ParseError"Known limitations
- Chat tab assumption. The library follows YouTube's default chat tab, which appears to be "Top chat" (algorithmically filtered subset) rather than "Live chat" (all messages chronologically). This is strongly suspected but not yet definitively proven. Tracking as an open issue. If you require all messages unfiltered, the official YouTube Data API is the reliable option.
Examples
Basic: listen to a channel's current stream
import { LiveChat } from "youtube-chat-next"
const liveChat = new LiveChat({ channelId: "UCxxxxxxxxxxxxxx" })
liveChat.on("start", (liveId) => {
console.log("Listening to:", liveId)
})
liveChat.on("chat", (chatItem) => {
console.log(`[${chatItem.author.name}] ${chatItem.message
.map(m => ("text" in m ? m.text : m.emojiText))
.join("")}`)
})
liveChat.on("end", (reason) => {
console.log("Stream ended:", reason)
})
liveChat.on("error", (err) => {
console.error("Error:", err.message)
})
await liveChat.start()
// Listen until stop() is calledLog to file with error handling
import { LiveChat, RateLimitError, ScrapeError } from "youtube-chat-next"
import fs from "fs"
const log = fs.createWriteStream("chat.log", { flags: "a" })
const liveChat = new LiveChat({ liveId: "KHRj-j02a1w" })
liveChat.on("chat", (chatItem) => {
const line = `${chatItem.timestamp.toISOString()} | ${chatItem.author.name}: ${
chatItem.message.map(m => ("text" in m ? m.text : m.emojiText)).join("")
}\n`
log.write(line)
})
liveChat.on("error", (err) => {
if (err instanceof RateLimitError) {
// Rate limited, back off and retry
console.warn("Rate limited. Waiting before retry...")
setTimeout(() => liveChat.start(), 30000)
} else if (err instanceof ScrapeError) {
// YouTube changed page structure, needs a code update
log.write(`CRITICAL: ${err.message}\n`)
} else {
log.write(`ERROR: ${String(err)}\n`)
}
})
await liveChat.start()References & credits
- Original library: youtube-chat by LinaTsukusu, thank you for the foundation.
- Reverse-engineering reference: https://drroot.page/wp/?p=227
- Python equivalent: https://github.com/taizan-hokuto/pytchat
This fork exists because the original is unmaintained. It preserves the spirit and API of the original while fixing real-world issues discovered over 3+ years of use.
License
MIT. See LICENSE for details. Original copyright by LinaTsukusu, modifications by contributors.
