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.pyThose 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:
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.
- Free YouTube Channel Scraper
- Avoid getting blocked when scraping YouTube channels
- YouTube Channel Scraper API reference
- List every video on a channel to CSV
- Measured latency
- License
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:
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.
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:
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.
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.
Below is the YouTube Channel Scraper API reference to get you started: authentication, the error bodies, the rate limits, and the endpoint itself.
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 30After running the command, your terminal should look something like this:
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.
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:
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.
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_countandvideo_countare rounded. They are parsed from the display strings (508M subscribers->508000000), so they carry YouTube's own rounding.subscriber_count_textandvideo_count_textkeep the original string.videosis 30 rows on page 1. Each row isposition,id,title,url,thumbnail,channel,views(a display string like"75,856,801 views") andpublished(a relative string like"3 weeks ago", not a date).keywordsis 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_urlandchannel_handlecarryhttp://, nothttps://, because they are passed through from YouTube's own microformat.has_moreplusnext_page_tokendrive pagination. Pass the token back aspage_tokento get the next 30, and repeat whilehas_moreis 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
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 3It 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:
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
Ten consecutive calls to /youtube/channel for @MrBeast, 3 seconds apart, on 2026-07-20. All ten returned 200:
| 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.
MIT. See LICENSE.







