YouTubeAnswer

How to check if a YouTube channel is live with an API

A live check through the YouTube Data API spends 1 of your 100 daily searches. Our API checks a channel's live streams and viewers for 1 credit, no quota.

6 min read

Short answer

  • The YouTube Data API finds a channel's live broadcast with search.list and eventType=live. Each call spends 1 of the default 100 searches a day.
  • videos.list reports liveBroadcastContent and concurrent viewers, but only for video IDs you already have.
  • Our YouTube API returns whether a channel is live and every live stream with its viewers, 1 credit a page and outside Google's quota.
On this page

The YouTube Data API can tell you a channel is live, but each check is a search. search.list with channelId, type=video and eventType=live returns the channel's active broadcasts, and searches have their own quota of 100 calls a day. Our YouTube API reads the channel's Live tab instead: whether the channel is live now, and each live stream with its viewer count, at 1 credit a call.

What the YouTube Data API is for

In the search.list reference, eventType=live limits results to active broadcasts and requires type=video. The quota page gives search.list its own bucket with "a default daily limit of 100 per day", apart from the 10,000 units for other methods. Checking one channel every 15 minutes is 96 searches a day. videos.list returns snippet.liveBroadcastContent (live, upcoming or none) and liveStreamingDetails.concurrentViewers, but only for video IDs you already have.

Live checks with ToolzerHub

We make ToolzerHub. Our YouTube API returns public YouTube channels, videos, comments and live streams as JSON with one API key and no Google project. /v2/youtube/channel/live-streams takes channel_username (an @handle) or channel_id, returns the channel's Live tab with page_cursor for the next page, and costs 1 credit a call.

bash
export TOOLZERHUB_API_KEY=your_key
curl -H "x-api-key: $TOOLZERHUB_API_KEY" \
  "https://api.toolzerhub.com/v2/youtube/channel/live-streams?channel_username=@LofiGirl"

data, trimmed to the channel and one stream:

json
{
  "meta": {
    "channelId": "UCSJ4gkVC6NrvII8umztf0Ow",
    "title": "Lofi Girl",
    "channelHandle": "@LofiGirl",
    "subscriberCount": 15800000,
    "isChannelLive": true
  },
  "data": [
    {
      "type": "video",
      "videoId": "rFZHOHl-L8A",
      "title": "lofi hip hop radio ๐Ÿ“š beats to relax/study to",
      "viewCountText": "16K watching",
      "viewCount": 16000,
      "isLive": true,
      "lengthText": "LIVE"
    }
  ]
}

meta.isChannelLive answers the question in one field; it is missing, not false, when the channel isn't live. Each live stream has isLive: true, and viewCount is the "watching" figure the page shows, rounded the same way, so 16K becomes 16000; videos.list reports concurrentViewers as a plain count. Past streams follow the live ones in the same list.

Check a list of channels

The script takes channel links, @handles or channel IDs, and reads further pages while a whole page is live, up to five. It writes one row per live stream with the channel, subscribers, title and viewers, or one row for a channel that isn't live, and prints how many channels are live. Install requests (pip install requests) and save it as youtube_live_check.py.

python
import csv
import os
import re
import sys
import time
from datetime import datetime, timezone

import requests

API = "https://api.toolzerhub.com/v2/youtube/channel/live-streams"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["checked_at", "channel", "handle", "subscribers", "channel_live", "video_id", "title", "watching", "url"]


def get(params, tries=3):
    # Retry rate limits, server errors and dropped connections.
    for attempt in range(tries):
        last = attempt == tries - 1
        try:
            r = requests.get(API, headers=HEADERS, params=params, timeout=120)
        except requests.RequestException:
            if last:
                raise
            time.sleep(5 * (attempt + 1))
            continue
        if r.status_code in (429, 500, 502, 503, 504) and not last:
            time.sleep(5 * (attempt + 1))
            continue
        r.raise_for_status()
        return r.json()["data"]


def channel_params(ref):
    # Accept a channel link, an @handle or a UC... channel ID.
    ref = ref.strip().split("?")[0].rstrip("/")
    m = re.search(r"(UC[\w-]{22})", ref)
    if m:
        return {"channel_id": m.group(1)}
    # In a link such as youtube.com/@LofiGirl/streams the handle is the part that starts with @.
    parts = [p for p in ref.split("/") if p]
    handle = next((p for p in reversed(parts) if p.startswith("@")), parts[-1])
    return {"channel_username": "@" + handle.lstrip("@")}


def main(channels, max_pages="5", out="youtube_live.csv"):
    live_channels = 0
    with open(out, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        refs = [c for c in channels.split(",") if c.strip()]
        for ref in refs:
            params, streams = channel_params(ref), []
            try:
                data = get(params)
                meta = data.get("meta") or {}
                # Live streams come first, so read further pages only while a whole page is live.
                for number in range(1, int(max_pages) + 1):
                    page = [v for v in data.get("data") or [] if v.get("type") == "video"]
                    streams += [v for v in page if v.get("isLive")]
                    whole_page_live = page and all(v.get("isLive") for v in page)
                    if number == int(max_pages) or not whole_page_live or not data.get("page_cursor"):
                        break
                    data = get({**params, "page_cursor": data["page_cursor"]})
            except requests.HTTPError as e:
                # A handle that doesn't exist fails upstream; skip it, but stop on key or credit errors.
                if e.response.status_code < 500:
                    raise
                print(f"{ref.strip()}: could not be read, check the handle")
                continue
            live_channels += bool(meta.get("isChannelLive"))
            base = {"checked_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "channel": meta.get("title"),
                    "handle": meta.get("channelHandle"), "subscribers": meta.get("subscriberCount"),
                    "channel_live": meta.get("isChannelLive")}
            # One row per live stream, or a single row for a channel that is not live.
            for v in streams or [{}]:
                writer.writerow({**base, "video_id": v.get("videoId"), "title": v.get("title"),
                                 "watching": v.get("viewCount"),
                                 "url": f"https://www.youtube.com/watch?v={v['videoId']}" if v else None})
            print(f"{meta.get('title')}: {'live' if meta.get('isChannelLive') else 'not live'}, "
                  f"{len(streams)} live streams, {sum(v.get('viewCount') or 0 for v in streams):,} watching")
    print(f"{live_channels} of {len(refs)} channels live")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit('usage: python youtube_live_check.py "@LofiGirl,@NASA" [max_pages] [out.csv]')
    if not HEADERS["x-api-key"]:
        sys.exit("set TOOLZERHUB_API_KEY first")
    main(*sys.argv[1:])

What 16 channels showed

python youtube_live_check.py "@LofiGirl,@SkyNews,@aljazeeraenglish,@NASA,@ABCNews,@France24_en,@dwnews,@SpaceX,@NASASpaceflight,@EarthCam,@CBSNews,@NBCNews,@markets,@ChillhopMusic,@MrBeast,@Kurzgesagt" made 17 calls in 15 seconds.

Channel Subscribers Live now Live streams Watching
Lofi Girl 15,800,000 yes 22 30,962
NBC News 12,200,000 yes 1 22,000
Al Jazeera English 18,300,000 yes 1 8,100
Sky News 9,390,000 yes 2 5,184
Bloomberg Television 3,330,000 yes 1 4,400
ABC News 19,900,000 yes 2 4,200
Chillhop Music 3,300,000 yes 4 1,438
CBS News 7,120,000 yes 1 1,400
NASASpaceflight 1,530,000 yes 3 1,394
FRANCE 24 English 3,700,000 yes 1 1,100
EarthCam 893,000 yes 44 836
DW News 6,490,000 yes 1 552
NASA 15,100,000 yes 2 264
SpaceX 7,010,000 no 0 0
MrBeast 525,000,000 no 0 0
Kurzgesagt 25,800,000 no 0 0

13 of the 16 were live, the news channels with one or two streams each. Lofi Girl ran 22 streams at once, and one of them, "lofi hip hop radio", had 16,000 of its 30,962 viewers. EarthCam's 44 cameras took two pages and showed 836 viewers between them, 217 on the busiest; 3 showed no count. Subscribers said little about who was watching: NBC News's election-results stream had 22,000, NASA's two streams from 15,100,000 subscribers had 264, and MrBeast, at 525,000,000, wasn't live.

Done through search.list, just finding which of them were live would use 16 of the day's 100 searches, with no viewer counts.

What it costs

A channel is 1 credit a page, so the sweep above was 17 credits. The 100 free credits a month cover five sweeps of these 16 channels; $19 buys 10,000 credits that never expire, and plans for higher volume cover checks every few minutes. Set your key and the first check is a CSV in under a minute.

For a channel's uploads, see get all videos from a YouTube channel. For live streams on TikTok, see the TikTok live API.

We are not affiliated with YouTube or Google.

Get this data from your own code

The YouTube API returns it as JSON with one x-api-key header. 100 free credits monthly.