🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

youtube-chat-next

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

youtube-chat-next

Fetch YouTube Live chat. Maintained fork of youtube-chat.

latest
Source
npmnpm
Version
3.1.0
Version published
Maintainers
1
Created
Source

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.list against 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.list is 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 error listener. Node's EventEmitter throws if an error event 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 RateLimitError and its Retry-After, lower your request volume (a larger interval), 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. Now interval is a floor; the library honours max(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, continuationContents disappears from YouTube's response. Upstream crashed with TypeError, swallowed it, and retried forever. Now emits an end event cleanly.

  • No overlapping requests. Replaced setInterval with 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 live
    • RateLimitError, 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-After header, 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-US so 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:live runs against live YouTube daily to catch regressions early.

Getting started

1. Install

npm install youtube-chat-next

or

yarn add youtube-chat-next

2. 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 } | EmojiItem

ImageItem

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 called

Log 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

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.

Keywords

youtube

FAQs

Package last updated on 16 Jul 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts