NewLaunching String Web Access APIRead the manifesto →
← Blog

How to scrape YouTube in 2026

Bruce Magness · October 12, 2026
Part of Best Web Scraping APIs in 2026: 16 Tools Compared

Last updated: October 12, 2026. Benchmark figures come from the September 16, 2026 run of the Web Data Frontier Benchmark. Every script on this page ran on October 12, 2026, and the output shown is what it printed.

To scrape YouTube in 2026, fetch the watch page or the channel page and read the JSON that YouTube puts inside the HTML. You do not need a browser. A video's title, view count, length, channel and publish date sit in a script variable called ytInitialPlayerResponse. A channel's video list sits in ytInitialData. On October 12, 2026, the Python below returned three videos with views and likes in 4.3 seconds, and the TypeScript returned a channel's 30 newest videos in 1.0 second. The page is rarely the hard part. Plain Python requests from a home connection got full data on 100 of 100 video pages. The hard parts are cloud IP addresses, which YouTube trusts less, and a page format that changes without notice.

This page is part of our series on the best web scraping APIs, tested.

On this page: Where String fits · What people run into · What 16 APIs did · Step 1: fetch the watch page · Step 2: read the video data · Step 3: list a channel's videos in TypeScript · The failure mode · How to avoid getting blocked · How String gets YouTube pages · What it costs · FAQ

TL;DR

  • YouTube is one of the easiest big sites to load. In the September 16, 2026 benchmark, 14 of 16 scraping APIs returned youtube.com on all five attempts. Only 9 of 100 sites were easier.
  • On October 12, 2026, plain Python requests from a US home connection got a full watch page on 100 of 100 videos, median 0.78 seconds each.
  • The data is JSON inside the HTML. ytInitialPlayerResponse holds the video details. ytInitialData holds the like count and the channel grid.
  • The block is about trust, not the page. Developers on servers report "Sign in to confirm you're not a bot" while the same command works on their laptop.
  • Through String, every YouTube page whose billing header we logged was a standard fetch: $0.30 per 1,000 on Starter and $0.20 on Growth.
  • If the YouTube Data API covers the fields you need, use it. Its default quota is 10,000 units a day, and a videos.list call costs 1 unit.

Where String fits

String is a fit for one job on YouTube: fetching public pages from servers, at volume, without managing IP trust.

  • For: developers and data teams that track channels, videos or view counts on a schedule, run from cloud servers, and want one API call per page.
  • Not for: downloading video or audio files, pages behind a login, or any job the YouTube Data API already covers within its quota.
  • Price: you pay only when content comes back. A YouTube page billed as a standard fetch in our runs: $0.30 per 1,000 on Starter ($20 a month) and $0.20 on Growth ($100 a month). The first 5,000 standard requests are free (pricing).
  • Integrations: a REST API you call from Python, TypeScript or any language, and an MCP server for agents.
  • Limits: 60 requests a second per account on Growth with a 3,600-request burst, and no cap on concurrency (pricing).

What people who scrape YouTube run into

The common complaint is the same error, on servers. A yt-dlp user wrote in February 2025: "yt-dlp is working perfectly on my local system. However, when I use it on my Amazon server, I am facing an issue" (yt-dlp issue #12475). The error was "Sign in to confirm you're not a bot." A yt-dlp contributor answered: "Youtube blocked anonymous access from many datacenter IPs."

The block is not only for servers. In February 2026, a user in the United States reported that every public video now came back LOGIN_REQUIRED on a Mac (yt-dlp issue #15865). A maintainer explained why a fresh client can fail where a browser works: "YouTube uses tracking cookies to determine how trustworthy your session is."

Two details matter for a scraper. First, these reports are about the player API that yt-dlp calls to get video streams. Second, the same trust signal shows up in the watch page, as playabilityStatus. The Python script below checks that field on every page, so a trust block stops the run instead of writing empty rows.

YouTube's supported route is the YouTube Data API. Its default quota is 10,000 units a day for most calls, plus 100 search.list calls. A videos.list call costs 1 unit (quota costs). If the API has your fields and your volume fits, use it.

Why a plain request to YouTube works, until it does not

We sent plain Python requests from a US home connection on October 12, 2026, with the default User-Agent and with a Chrome User-Agent.

What we tried, October 12, 2026 Result
Watch page, https://www.youtube.com/watch?v=dQw4w9WgXcQ HTTP 200, 1.3 MB, video data present, both User-Agents
Channel page, https://www.youtube.com/@RickAstleyYT/videos HTTP 200, 1.3 MB, channel grid present, both User-Agents
100 different watch pages, one after another, Chrome User-Agent 100 of 100 returned playabilityStatus "OK" with a view count; median 0.78 s, 78.7 s in total

So a home connection at low volume needs no help. The trouble starts when the same code moves to a cloud server, runs at volume, or keeps a session YouTube scores as new. Most of the yt-dlp reports above describe that move. We did not test a datacenter IP ourselves on this run.

What 16 scraping APIs did with youtube.com

The Web Data Frontier Benchmark sends the same 100 URLs to 16 scraping APIs, five attempts each. The YouTube URL is a watch page. The harness and targets are open source, and so is the raw result file for the September 16 run.

API youtube.com, 5 attempts Average time on youtube.com Overall success, 100 sites
String 100% 5.7 s 97.0%
Scrapfly 100% 6.2 s 86.2%
ScraperAPI 100% 16.6 s 84.0%
Firecrawl 100% 8.8 s 80.2%
Apify 100% 6.7 s 77.4%
Bright Data 100% 4.3 s 74.6%
ScrapingBee 100% 1.8 s 73.0%
Context.dev 100% 8.1 s 72.0%
Oxylabs 100% 3.5 s 69.0%
Nimble 100% 4.6 s 68.6%
Zyte 100% 2.1 s 68.0%
Decodo 100% 25.9 s 50.6%
Scrapingdog 0% none passed 45.6%
Browserbase 100% 2.7 s 41.4%
ZenRows 0% none passed 41.2%
ScrapingAnt 100% 19.3 s 36.4%

Fourteen of 16 APIs returned youtube.com on all five attempts: 70 passes out of 80 requests. The average API passed 87.5% of attempts on YouTube, against 66.6% across all 100 sites. Only 9 of 100 sites scored higher.

Read the two zeros with care. ZenRows' API closed the connection without a response on every attempt from the 59th target of the suite to the end, and YouTube is the 61st, so its zero measures the ZenRows API, not YouTube. Scrapingdog's zero has no such block. Scrapfly, Firecrawl, ScraperAPI, Zyte, ScrapingAnt, ScrapingBee, Scrapingdog and Nimble ran on harness settings below their current anti-bot mode, because the harness changed on September 6, 2026. The benchmark counts a pass when the response is a 2xx and the body contains "Rick Astley".

The point of the table is the spread at the top, not the bottom. On YouTube, most APIs get the page. What separates them is speed, from 1.8 seconds to 25.9 seconds, and what you do with the page after it arrives.

Try it on your own list: pip install requests, set STRING_API_KEY, and run the script in Step 2. The first 5,000 standard requests are free.

Step 1: fetch the watch page

The fetch is one POST to String's fetch endpoint with the YouTube URL.

import os
import requests

def fetch_html(url: str) -> str:
    response = requests.post(
        "https://request.usestring.ai/v1/fetch",
        headers={"Authorization": f"Bearer {os.environ['STRING_API_KEY']}"},
        json={"url": url, "countryCode": "US"},
        timeout=90,
    )
    response.raise_for_status()
    page = response.json()
    if page["statusCode"] != 200:
        raise RuntimeError(f"youtube.com returned {page['statusCode']} for {url}")
    return page["data"]

statusCode is YouTube's own status, not String's. Pin countryCode on every request. YouTube changes the page's language and some fields by country, so a run that drifts between countries compares different pages.

Step 2: read the video data

The watch page holds two JSON objects, and the script reads one of them whole.

ytInitialPlayerResponse has the fields most people want: videoDetails (title, channel, length, view count, description, keywords) and microformat (publish date, category). The like count is not there. It sits in the like button's label in ytInitialData, as text such as "like this video along with 19,485,308 other people".

Here is the whole script:

"""Scrape YouTube video metadata with the String Web Access API.

    pip install requests
    export STRING_API_KEY=...
    python youtube_video.py dQw4w9WgXcQ 9bZkp7q19f0
"""
import json
import os
import re
import sys

import requests

API = "https://request.usestring.ai/v1/fetch"


def fetch_html(url: str) -> str:
    response = requests.post(
        API,
        headers={"Authorization": f"Bearer {os.environ['STRING_API_KEY']}"},
        json={"url": url, "countryCode": "US"},
        timeout=90,
    )
    response.raise_for_status()
    page = response.json()
    if page["statusCode"] != 200:
        raise RuntimeError(f"youtube.com returned {page['statusCode']} for {url}")
    return page["data"]


def json_blob(html: str, name: str) -> dict:
    # YouTube ships the page's data as a JavaScript object assigned to a variable.
    match = re.search(rf"var {name}\s*=\s*(\{{.*?\}});\s*(?:var |</script>)", html, re.S)
    if not match:
        raise RuntimeError(f"{name} not found: a consent page or a block, not a watch page")
    return json.loads(match.group(1))


def video(video_id: str) -> dict:
    html = fetch_html(f"https://www.youtube.com/watch?v={video_id}")
    player = json_blob(html, "ytInitialPlayerResponse")
    status = player["playabilityStatus"]["status"]
    if status != "OK":
        raise RuntimeError(f"{video_id}: playability {status}: {player['playabilityStatus'].get('reason')}")
    details = player["videoDetails"]
    micro = player["microformat"]["playerMicroformatRenderer"]
    # The like count is not in videoDetails. It sits in the like button's label in ytInitialData:
    # "like this video along with 19,485,309 other people".
    likes = re.search(r"along with ([\d,]+) other", html)
    return {
        "id": details["videoId"],
        "title": details["title"],
        "channel": details["author"],
        "channel_id": details["channelId"],
        "seconds": int(details["lengthSeconds"]),
        "views": int(details["viewCount"]),
        "likes": int(likes.group(1).replace(",", "")) if likes else None,
        "published": micro.get("publishDate", "")[:10],
        "category": micro.get("category"),
        "description_chars": len(details.get("shortDescription", "")),
        "keywords": len(details.get("keywords", [])),
    }


if __name__ == "__main__":
    ids = sys.argv[1:] or ["dQw4w9WgXcQ"]
    for vid in ids:
        row = video(vid)
        print(f'{row["id"]}  {row["views"]:>13,} views  {row["likes"] or 0:>11,} likes  {row["published"]}  {row["seconds"]:>4}s  {row["title"][:48]}')

What it printed on October 12, 2026, in 4.3 seconds for three videos:

dQw4w9WgXcQ  1,825,821,740 views   19,485,308 likes  2009-10-24   213s  Rick Astley - Never Gonna Give You Up (Official 
9bZkp7q19f0  6,089,933,598 views   32,305,311 likes  2012-07-15   252s  PSY - GANGNAM STYLE(강남스타일) M/V
kJQP7kiw5Fk  9,144,152,351 views   56,751,643 likes  2017-01-12   282s  Luis Fonsi - Despacito ft. Daddy Yankee

The script raises an error, not a blank row, in two cases: when ytInitialPlayerResponse is missing, and when playabilityStatus is anything but "OK". The second check is the one that catches "Sign in to confirm you're not a bot", which yt-dlp users see as LOGIN_REQUIRED in that field.

These fields came back on all three videos:

Field Where it is Example from the run
Title, channel, channel ID videoDetails Rick Astley, UCuAXFkgsw1L7xaCfnd5JJOw
Length in seconds videoDetails.lengthSeconds 213
View count videoDetails.viewCount 1,825,821,740
Like count like button label in ytInitialData 19,485,308
Publish date, category microformat.playerMicroformatRenderer 2009-10-24, Music
Description, keywords videoDetails.shortDescription, keywords full text
Caption tracks captions in the player response 6 tracks listed on this video

Views moved between two fetches a few minutes apart, from 1,825,821,169 to 1,825,821,740. Store the time of each fetch next to the number.

Step 3: list a channel's videos in TypeScript

A channel's /videos tab holds its newest 30 uploads in ytInitialData. No dependencies. Node 22.6 or later runs TypeScript directly (we ran it on Node 24), and so does Bun.

// List the newest videos on a YouTube channel with the String Web Access API.
//   export STRING_API_KEY=...
//   node youtube_channel.ts @RickAstleyYT      (Node 22.6+; or: bun youtube_channel.ts @RickAstleyYT)

async function fetchHtml(url: string): Promise<string> {
  const response = await fetch("https://request.usestring.ai/v1/fetch", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.STRING_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ url, countryCode: "US" }),
  });
  if (!response.ok) throw new Error(`String returned ${response.status}: ${await response.text()}`);
  const page = (await response.json()) as { statusCode: number; data: string };
  if (page.statusCode !== 200) throw new Error(`youtube.com returned ${page.statusCode}`);
  return page.data;
}

// Collect every object stored under `key`, at any depth.
function findAll(node: unknown, key: string, out: any[] = []): any[] {
  if (Array.isArray(node)) node.forEach((n) => findAll(n, key, out));
  else if (node && typeof node === "object") {
    for (const [k, v] of Object.entries(node)) {
      if (k === key) out.push(v);
      findAll(v, key, out);
    }
  }
  return out;
}

const handle = process.argv[2] ?? "@RickAstleyYT";
const html = await fetchHtml(`https://www.youtube.com/${handle}/videos`);
const match = html.match(/var ytInitialData\s*=\s*(\{.*?\});\s*<\/script>/s);
if (!match) throw new Error("ytInitialData not found: a consent page or a block, not a channel page");
const data = JSON.parse(match[1]);

// The channel grid uses lockupViewModel; older scrapers look for videoRenderer.
const videos = findAll(data, "lockupViewModel")
  .filter((l) => l.contentType === "LOCKUP_CONTENT_TYPE_VIDEO")
  .map((l) => {
    const meta = l.metadata.lockupMetadataViewModel;
    const parts = meta.metadata.contentMetadataViewModel.metadataRows[0].metadataParts;
    const badge = findAll(l.contentImage, "thumbnailBadgeViewModel")[0];
    return { id: l.contentId, title: meta.title.content, views: parts[0]?.accessibilityLabel, age: parts[1]?.text.content, length: badge?.text };
  });

const more = findAll(data, "continuationCommand").length > 0;
console.log(`${handle}: ${videos.length} videos on the first page, more pages: ${more}`);
for (const v of videos.slice(0, 5)) console.log(`${v.id}  ${v.length?.padStart(5)}  ${v.age?.padEnd(9)} ${v.views?.padEnd(20)} ${v.title.slice(0, 50)}`);

We ran it on October 12, 2026, on Node 24. It returned in 1.0 second:

@RickAstleyYT: 30 videos on the first page, more pages: true
PXC_PYeB6F8   3:51  3mo ago   105 thousand views   Rick Astley - Angels On My Side (Live at The O2, L
LaOUkDBDjW8   3:54  3mo ago   61 thousand views    Rick Astley - Dance (Live at The O2, London, April
wvr7-pDJUOA   4:08  3mo ago   96 thousand views    Rick Astley - Keep Singing (Live at The O2, London
dE2TxYSDy4Y  47:18  3mo ago   40 thousand views    Rick Astley - '50' Track By Track (10 Years Later)
LjmOX9jGoR4   3:36  5mo ago   135 thousand views   Rick Astley - Raindrops (Official Video)

On Bun, the same script returned 30 videos for @mkbhd in 1.0 second. The grid gives rounded views ("105 thousand") and relative ages ("3mo ago"). For exact counts and dates, pass each ID to the Python script in Step 2.

The grid stops at 30. Older uploads load through YouTube's internal /youtubei/ API, which YouTube's robots.txt disallows, so this page does not script it. The Data API's playlistItems.list on the channel's uploads playlist lists every upload at 1 unit a call.

The failure mode: an empty list with no error

YouTube's quiet failure is a format change, not a block.

On October 12, 2026, the channel grid used lockupViewModel for all 30 videos and had no videoRenderer at all. A scraper written for videoRenderer gets HTTP 200, a 1.3 MB page, and zero videos, every day, with no error.

Three checks catch it:

  • Count what you parsed. A channel tab with zero videos is a format change or a block. Log the URL and look at the page.
  • Check playabilityStatus on watch pages. "OK" is a page with data. LOGIN_REQUIRED is the bot check, whatever the HTTP status says.
  • Fail when the JSON is missing. Both scripts raise an error when ytInitialPlayerResponse or ytInitialData is not in the HTML, because a consent page or a block page never carries them.

How to avoid getting blocked on YouTube

YouTube scores the client, the IP address and the session together.

  • Keep the IP's reputation in mind. A home connection loaded 100 of 100 pages in our test. Developers report "Sign in to confirm you're not a bot" most often from cloud servers, per the yt-dlp reports above.
  • Fetch pages, not streams. The yt-dlp reports are about the player API that downloaders call for video streams. Metadata does not need the stream, and our watch-page run hit no bot check.
  • Stay on the paths robots.txt allows. /watch and channel pages are allowed. /results, /youtubei/ and /api/ are disallowed (robots.txt).
  • Pace the run. Sequential requests with a short pause are enough for most monitoring jobs. Our 100 pages took 79 seconds one at a time.
  • Do not lean on exported cookies. A yt-dlp contributor notes that cookies rotate as soon as you use YouTube in the browser, which makes exported cookies invalid (issue #12475). The same contributor warns that "google will block/ban accounts for abusive behavior."

When your scraper runs on a server and starts to see LOGIN_REQUIRED, send the same URL through a fetch API instead: curl -X POST https://request.usestring.ai/v1/fetch -H "Authorization: Bearer $STRING_API_KEY" -d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}'.

How String gets YouTube pages

String is the most reliable web scraping API, bypassing anti-bot, and you only pay when content comes back. Here is what that means on YouTube, one fact per line:

  • String returned youtube.com on every attempt. In the September 16, 2026 benchmark run, String passed 5 of 5 on YouTube in 5.7 seconds on average, and 97.0% across all 100 sites, the highest of 16 APIs (benchmark).
  • One call returns the whole page. POST /v1/fetch returns the 1.3 MB watch page with both JSON objects in it, so the scripts on this page parse it locally (fetch docs).
  • YouTube pages bill at the lowest rate. Every YouTube request we logged on October 12, 2026 returned the x-billed-request-type header request_standard: $0.30 per 1,000 on Starter.
  • The site's own status comes back separately. statusCode in the response body is YouTube's status, so a real 404 for a deleted video stays a 404.
  • A failed request costs nothing. String bills only when content comes back.
  • Agents get the same fetch as a tool. String's hosted MCP server gives Claude and other MCP clients fetch and search with the same anti-bot handling.

What it costs

Every YouTube request whose billing header we logged on October 12, 2026, two watch pages and a channel page, billed as a standard fetch. From String's pricing, that is $0.30 per 1,000 requests on Starter ($20 a month) and $0.20 per 1,000 on Growth ($100 a month). The first 5,000 standard requests are free.

Worked through from the runs above:

  • Video monitoring. One request per video. Checking 10,000 videos a day is about 300,000 requests a month: $90 on Starter or $60 on Growth.
  • Channel tracking. One request returns a channel's 30 newest videos. Checking 1,000 channels a day is about 30,000 requests a month: $9 on Starter or $6 on Growth.

Price the YouTube Data API first. Its default quota is free, and one videos.list call returns up to 50 videos for 1 unit.

Is it legal to scrape YouTube?

YouTube's terms forbid it without permission. The YouTube Terms of Service say you may not "access the Service using any automated means (such as robots, botnets or scrapers)", except public search engines that follow robots.txt or with YouTube's prior written permission. The terms also restrict collecting information that might identify a person.

Breaking a site's terms is a contract question, separate from whether the data is public. Our page on whether web scraping is legal covers the cases. This is not legal advice. If the Data API covers your use, it is the route YouTube supports.

FAQ

How to scrape YouTube video data with Python?

Fetch the watch page and parse ytInitialPlayerResponse, a JSON object inside the HTML. Its videoDetails has title, channel, length, view count, description and keywords, and microformat has the publish date. The Python script on this page returned three videos with views and likes in 4.3 seconds on October 12, 2026.

Can I scrape data from YouTube?

Public pages, yes, technically: on October 12, 2026, plain Python got full data on 100 of 100 watch pages from a home connection. YouTube's terms forbid automated access without permission, and its supported route is the YouTube Data API.

Is YouTube's API free?

Yes, within quota. Projects get a default of 10,000 units a day for most calls, plus 100 search.list calls a day (Google). A videos.list call costs 1 unit and takes up to 50 video IDs.

How do I scrape a YouTube channel's videos?

Fetch https://www.youtube.com/@handle/videos and read ytInitialData. On October 12, 2026, the grid held the 30 newest uploads as lockupViewModel objects. The TypeScript script on this page returned them in 1.0 second. Older uploads need the Data API's uploads playlist.

Why does YouTube say "Sign in to confirm you're not a bot"?

YouTube does not trust the client's IP address or session enough for anonymous access. Developers report it most from cloud servers. A yt-dlp maintainer put it this way: YouTube "does not trust your IP address enough to allow anonymous access" (issue #12475). yt-dlp logs show it as playabilityStatus LOGIN_REQUIRED, the field the Python script on this page checks.

How do I get the like count from a YouTube page?

The like count is not in videoDetails. It is in the like button's label inside ytInitialData, as text such as "like this video along with 19,485,308 other people". The Python script reads it with one regular expression.

How to get the transcript of a YouTube video using Python?

The player response lists the caption tracks under captions; our test video listed six. The track URLs sit under /api/timedtext, which YouTube's robots.txt disallows, so this page does not script them. The Data API's captions.download works only for videos you own.

Can I scrape YouTube comments?

Comments are not in the first page load. They arrive through YouTube's internal /youtubei/ API, which robots.txt disallows. The Data API's commentThreads.list returns them at 1 unit a call (quota costs).

Sources

Cheers,
String team

Get your API key →Explore the Web Access API
© 2026 StringEU and UK GDPR Article 27 representative — appointment verified by EuverifyBuilt in New York City 🗽 🍎