Skip to content

ChocoData-com/youtube-channel-scraper

Repository files navigation

YouTube Channel Scraper

YouTube Channel Scraper

YouTube Channel Scraper for extracting subscriber counts, video counts, descriptions and a channel's full video list from YouTube.com. This repo has a free YouTube channel web scraping script you can run right now, and a YouTube channel data API that returns 21 structured fields plus the channel's videos, paged with a cursor.

This is the channel endpoint on its own. The YouTube Scraper repo covers the rest of the surface: search, videos, playlists, transcripts, comments and Shorts.

Last updated: 2026-07-20. Working against YouTube.com as of July 2026, and re-verified whenever YouTube changes their markup.

Below is the YouTube Channel Scraper to get you started. Every JSON block on this page was captured from the live API on 2026-07-20. Long arrays are trimmed and each block says exactly what was cut; the fields shown are verbatim. Full uncut samples are committed in youtube_channel_scraper_api_data/. Every code example calls the actual API and is runnable from youtube_channel_scraper_api_codes/.

pip install requests
export CHOCODATA_API_KEY="your_key"     # free: 1,000 requests, one-time, no card
python youtube_channel_scraper_api_codes/channel.py

Those three lines return this, live from YouTube.com:

{
  "channel_name": "MrBeast",
  "channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
  "handle": "@MrBeast",
  "subscriber_count": 508000000,
  "video_count": 994,
  "videos_count": 30,
  "has_more": true
}

That is 7 of the 21 fields on page 1, plus 30 video rows. Here are all of them:

Retrieved YouTube channel data

That is the whole point of this repo. The rest of this page is the reference: the parameters, the real response, and the fields worth knowing about before you build on them.


Contents


Free YouTube Channel Scraper

YouTube server-renders a channel page's whole state into a JavaScript object called ytInitialData. The channel's identity and description sit in metadata.channelMetadataRenderer; the video grid sits further down in richGridRenderer. So you can extract structured channel data without a headless browser or JavaScript rendering. No key, no cost:

python free_scraper/youtube_channel_free_scraper.py "@MrBeast"

Source: free_scraper/youtube_channel_free_scraper.py. It resolves the channel reference to a path, fetches the tab, brace-counts the ytInitialData object out of the HTML, and reads the metadata plus every video row in the grid (the grid ships as four different renderer shapes, so it handles all of them and de-duplicates on the video id).

After running the command, your terminal should look something like this:

Free YouTube channel scraper parsing a real channel page

It works, once you clear the consent gate. A cold request to a channel URL is redirected to YouTube's consent interstitial (<title>Before you continue to YouTube</title>), a ~584 KB page that carries no ytInitialData at all. That is the standard EU-style consent gate, not a bot block: the same client fetches a watch page fine. Setting the standard consent cookie (SOCS=CAI) clears it, and then the channel page returns normally. With the cookie in place, 6 of 6 consecutive attempts succeeded in a measured run on 2026-07-20, 4 seconds apart, at 1,161,833 to 1,163,479 bytes each, 30 videos per attempt.

What actually costs you time on channel pages is a different set of problems, and it is the subject of the next section.

Avoid getting blocked when scraping YouTube channels

The consent redirect above is a cookie, not a wall. These are the things that genuinely bite a channel scraper, and all of them are measured against YouTube on 2026-07-20.

First, one page is 30 videos and a channel has hundreds. video_count on @MrBeast is 994; page 1 of the grid returns 30. The full list only comes from following the pagination cursor (next_page_token) until has_more is false, which is 34 requests for this channel. If your plan was "read a channel's videos in one call", it does not survive contact with a channel that posts more than a page.

Second, the counts are the display figure, not the exact one. subscriber_count comes back 508000000 because the page shows "508M subscribers". video_count is 994 on @MrBeast but 6100 on @NASA, which shows "6.1K videos". These are the rounded strings YouTube renders, parsed to integers, so treat subscriber_count as a rounded figure and never diff two reads of it expecting single-subscriber precision.

Third, tab=shorts returns an empty grid. On @MrBeast, @fireship, @NASA and @mkbhd, tab=shorts returned 0 video rows on 2026-07-20 while the channel metadata came back fine. The Shorts grid ships in a renderer shape the endpoint does not currently list. tab=videos and tab=streams return rows; tab=shorts returns the header and an empty videos array.

Here is the full picture, and what each item costs you:

What bites you Why What it costs you
A cold client gets the consent page A first request to a channel URL is redirected to Before you continue to YouTube (~584 KB, no ytInitialData). Every request needs consent-cookie handling before you see any data. The watch page does not have this problem, so it is easy to miss until every channel comes back empty.
30 videos per page The grid is paginated; video_count is the channel total. A channel with 994 videos is 34 sequential requests, each depending on the previous page's cursor. No parallelism within one channel.
Counts are rounded display text 508000000 is parsed from "508M subscribers", not an exact count. Subscriber and video counts are approximate. A growth chart built on them steps in millions, not ones.
tab=shorts returns 0 rows The Shorts grid uses a renderer the endpoint does not list yet. Shorts inventory is not reachable through this endpoint today. videos and streams are.
The grid shape rotates The same tab ships as videoRenderer, gridVideoRenderer, compactVideoRenderer or lockupViewModel depending on the client and region. A parser that reads one shape silently returns nothing when YouTube serves another.

So the two paths, side by side, same channel, same day:

Free YouTube channel scraper vs Chocodata API, measured

Both reach the channel once the consent gate is handled. The difference is what you carry: 1.16 MB of HTML and a grid walker that has to keep matching four renderer shapes, versus 17 KB of JSON with 21 fields named and the pagination cursor already minted. On the grid layout a plain client received, the view counts also came back as the rounded display string ("75M views"); the API returned the full count ("75,866,587 views") for the same 30 video ids the same day.


Using the Chocodata YouTube Channel Scraper API

The managed option, and the one this repo is built around: the Chocodata YouTube Channel Scraper API. One GET request per channel, 21 fields of YouTube channel data extraction at scale, a ~99% success rate, consent handling and proxy management done for you, and cursor pagination through every page of videos. Free for the first 1,000 requests.


YouTube Channel Scraper API reference

Below is the YouTube Channel Scraper API reference to get you started: authentication, the error bodies, the rate limits, and the endpoint itself.

Quickstart

curl "https://api.chocodata.com/api/v1/youtube/channel?api_key=YOUR_KEY&channel=@MrBeast&tab=videos"
import requests

r = requests.get(
    "https://api.chocodata.com/api/v1/youtube/channel",
    params={"api_key": "YOUR_KEY", "channel": "@MrBeast", "tab": "videos"},
    timeout=90,
)
c = r.json()
print(c["channel_name"], c["subscriber_count_text"], c["videos_count"])
# MrBeast 508M subscribers 30

After running the command, your terminal should look something like this:

Running the YouTube Channel Scraper API

Authentication

Pass your key as the api_key query parameter. There is no header form and no OAuth step.

https://api.chocodata.com/api/v1/youtube/channel?api_key=YOUR_KEY&channel=@MrBeast

A free key is 1,000 requests, one time, with no card. Get one at chocodata.com.

Errors

Nothing below is billed: you are only charged on a 2xx.

Status error code Meaning Billed What to do
400 invalid_params Neither channel nor page_token was supplied. The body names the missing param and its path. no Fix the query string.
401 INVALID_API_KEY Key missing, unrecognised, or revoked. no Check api_key. Get one at chocodata.com.
402 INSUFFICIENT_CREDITS Balance exhausted. no Top up or upgrade.
429 RATE_LIMITED Over 120 requests/60s, or over your plan's concurrency. no Back off and retry.
502 - YouTube did not return a parseable channel page for this request. A nonexistent handle lands here too, not on a 404. no Check the handle, then retry once after a few seconds.

A bad key, verbatim:

curl "https://api.chocodata.com/api/v1/youtube/channel?api_key=totally_invalid_key&channel=@MrBeast"
{"error": {"code": "INVALID_API_KEY", "message": "Api key not recognised."}}

A missing required param, verbatim. Note that it names the real parameter:

{"error": "invalid_params", "issues": [{"code": "custom", "message": "youtube.channel requires `channel` or `page_token`", "path": ["channel"]}]}

Two response shapes exist: auth and billing errors nest under error.code (uppercase), while param errors are flat with a lowercase error string and an issues array. The scripts in this repo map the documented statuses onto actionable messages, so a typo'd key does not hand you a stack trace:

YouTube Channel Scraper API error handling

Rate limits and concurrency

Two separate limits apply, and they are enforced independently.

Limit Value
Requests per key 120 per 60 seconds (sliding window)
Concurrent requests, Free 10
Concurrent requests, Vibe 30
Concurrent requests, Pro 50
Concurrent requests, Custom 100

Exceed either and you get 429, not a queue. Every call is a synchronous GET: there is no webhook, callback, or async job to poll. Paging one channel is sequential because each page_token comes from the previous page, so a channel with hundreds of videos is bound by the per-request latency, not concurrency. Fan out across different channels with a thread pool, sized to stay inside both limits at once.

Channel: subscriber counts, video counts and the video list

Page 1 of a channel: its identity, subscriber and video counts, description, keywords, and the first 30 videos on the requested tab, plus a cursor for the next page.

Param Type Required Default Description
channel string one of channel/page_token - A handle (@MrBeast or MrBeast), a /channel/UC... id, a /c/Name or /user/Name legacy path, a bare UC... id, or any full channel URL. All resolve to the same channel.
tab enum no videos videos, shorts, streams or about. videos and streams return the grid; shorts returned 0 rows on every channel sampled on 2026-07-20; about fetches the channel home page. Description and keywords come back on every tab.
country string (ISO-2) no - Two-letter country the request is routed from, e.g. US, GB, DE.
page_token string no - The next_page_token from a previous call. When present, channel is not required and exactly one further page is returned.
api_key string yes - Your key. Query parameter, not a header.
curl "https://api.chocodata.com/api/v1/youtube/channel?api_key=YOUR_KEY&channel=@MrBeast&tab=videos"

Real response. description truncated from 972 chars where marked, videos cut to 2 of 30, and the next_page_token/next_page_url cursor truncated. All 21 fields are present, and every value not marked as truncated is verbatim (full sample):

{
  "channel": "MrBeast",
  "channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
  "channel_name": "MrBeast",
  "handle": "@MrBeast",
  "vanity_url": "http://www.youtube.com/@MrBeast",
  "url": "https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA",
  "tab": "videos",
  "description": "SUBSCRIBE FOR A COOKIE!\nNew MrBeast or MrBeast Gaming video every single Satur ...(truncated from 972 chars)",
  "keywords": "mrbeast6000 beast mrbeast Mr.Beast mr",
  "avatar": "https://yt3.googleusercontent.com/nxYrc_1_2f77DoBadyxMTmv7ZpRZapHR5jbuYe7PlPd5cIRJxtNNEYyOC0ZsxaDyJJzXrnJiuDE=s900-c-k-c0x00ffffff-no-rj",
  "is_family_safe": true,
  "subscriber_count": 508000000,
  "subscriber_count_text": "508M subscribers",
  "video_count": 994,
  "video_count_text": "994 videos",
  "videos": [
    {
      "position": 1,
      "id": "iYlODtkyw_I",
      "title": "Survive 30 Days Chained To A Stranger, Win $250,000",
      "url": "https://www.youtube.com/watch?v=iYlODtkyw_I",
      "thumbnail": "https://i.ytimg.com/vi/iYlODtkyw_I/hq720.jpg?sqp=-oaymwEhCK4FEIIDSFryq4qpAxMIARUAAAAAGAElAADIQj0AgKJD&rs=AOn4CLB7AiG1nps9yMHQMJg-9Pji8f3YjA",
      "channel": "MrBeast",
      "views": "75,856,801 views",
      "published": "3 weeks ago"
    },
    {
      "position": 2,
      "id": "__fmDj0ZJ1Q",
      "title": "50 YouTube Legends Fight For $1,000,000",
      "url": "https://www.youtube.com/watch?v=__fmDj0ZJ1Q",
      "thumbnail": "https://i.ytimg.com/vi/__fmDj0ZJ1Q/hq720.jpg?sqp=-oaymwEhCK4FEIIDSFryq4qpAxMIARUAAAAAGAElAADIQj0AgKJD&rs=AOn4CLAYTCBDVSncERnpgfTJUVoe7Z_A3g",
      "channel": "MrBeast",
      "views": "74,268,142 views",
      "published": "1 month ago"
    }
  ],
  "videos_count": 30,
  "page": 1,
  "next_page_token": "eyJjIjoiNHFtRnNnTFpDUklZVlVO...(truncated)",
  "next_page_url": "/api/v1/youtube/channel?page_token=eyJjIjoiN...(truncated)",
  "has_more": true
}

channel_id is the field most people come for: the UC... id is the stable key that survives a handle rename, and it is the identifier the video and other YouTube endpoints accept. A few notes on the rest, each measured across the channels sampled on 2026-07-20:

  • subscriber_count and video_count are rounded. They are parsed from the display strings (508M subscribers -> 508000000), so they carry YouTube's own rounding. subscriber_count_text and video_count_text keep the original string.
  • videos is 30 rows on page 1. Each row is position, id, title, url, thumbnail, channel, views (a display string like "75,856,801 views") and published (a relative string like "3 weeks ago", not a date).
  • keywords is a single space-joined string, not an array. It is the channel's own tag list, so it is uploader-controlled and can be empty.
  • vanity_url and channel_handle carry http://, not https://, because they are passed through from YouTube's own microformat.
  • has_more plus next_page_token drive pagination. Pass the token back as page_token to get the next 30, and repeat while has_more is true.

A second committed sample, channel_nasa.json, is @NASA, where video_count_text is "6.1K videos" and video_count is 6100 (the rounded parse). A page-2 continuation is in channel_page2.json: fetched with page_token, it carries 30 fresh videos with 0 ids overlapping page 1.

Runnable: youtube_channel_scraper_api_codes/channel.py


List every video on a channel to CSV

The job most people arrive with: point it at a channel and get one row per video across every page, titles and view-count strings included.

export CHOCODATA_API_KEY="your_key"
python youtube_channel_scraper_api_codes/list_channel_videos.py @NASA --max-pages 3

It reads page 1, then follows next_page_token while has_more is true, sleeping 1.5s between calls to stay inside the 120 requests/60s limit and retrying a lone 502 once after 8 seconds. The output is a CSV, one row per video:

Listing every video on a channel

The CSV carries position, id, title, url, views, published. Drop --max-pages to walk the whole channel; on @MrBeast that is 34 pages for all 994 videos.

Source: youtube_channel_scraper_api_codes/list_channel_videos.py


Measured latency

Ten consecutive calls to /youtube/channel for @MrBeast, 3 seconds apart, on 2026-07-20. All ten returned 200:

Measured latency for the YouTube channel endpoint

Metric Value
calls attempted 10
200 responses 10
min 2,352 ms
median 3,943 ms
max 7,363 ms

Ten calls is a small sample and the spread is wide, which is why the examples set timeout=90 rather than sizing to the median, and why the CSV script retries a 502 once before giving up.


License

MIT. See LICENSE.