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.
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:
{
"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.
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.