YouTube Search Scraper
Pricing
$19.99/month + usage
YouTube Search Scraper
YouTube Search Scraper API extracts data from any YouTube channel, including video lists, titles, views, likes, upload dates, and metadata. Great for research, competitor analysis, or content tracking. Export structured results in JSON, CSV, or Excel for seamless analysis and reporting.
Pricing
$19.99/month + usage
Rating
5.0
(1)
Developer
Scraper Engine
Maintained by CommunityActor stats
2
Bookmarked
54
Total users
0
Monthly active users
12 hours ago
Last modified
Categories
Share
YouTube Search Scraper — Views, Likes, Tags and Channel Data
YouTube Search Scraper turns a keyword into a dataset of public YouTube search results: title, watch URL, absolute view count, likes, comment count, the full untruncated description, tags, hashtags, duration, an exact publish timestamp and the channel behind every result. Sort, upload-date, duration and result-type filters are sent to YouTube as real search parameters. Paste a video or channel URL into the same field and it is fetched directly instead. Every row arrives as typed JSON — no HTML, no selectors.
After reading this you will know exactly which keys land in your dataset, which rows are charged and which are not, and what the run does before you have spent a byte.
⚠️ An empty run is not an empty run. queries carries a schema default of ["python programming"], so starting the Actor without touching that field runs that search and bills for the rows it returns. Replace the default with your own queries before the first run. If you instead clear the field completely — an empty list, or only blank lines, and no URLs — the Actor writes one uncharged row, {"type": "error", "error": "No search queries or URLs provided."}, and stops.
⚠️ Full video extraction is bot-walled on datacenter IPs. Likes, comment counts, tags and full descriptions come from the per-video payload, and the Actor's own schema records the measured result on a datacenter exit: zero rows, and YouTube replying "Sign in to confirm you're not a bot." So when type is video, the run tries your proxy first and escalates to Apify Residential if YouTube blocks it — and starts on Residential directly when you have selected no proxy. type: channel and type: playlist read the search page only and need no proxy at all.
What is YouTube Search Scraper?
YouTube Search Scraper is an Apify Actor that runs keyword searches against youtube.com/results and writes every public result to a dataset as one typed row. The same input field also accepts YouTube URLs: a watch?v=, /shorts/ or youtu.be/ link returns that single video in full, and a channel link returns that channel's About page — description, country, joined date, and the external link list YouTube renders there.
It runs signed out end to end. No YouTube account, no Google login, no Google Cloud project and no API key are used or requested; the only credential involved is your Apify token.
It is built for SEO and content researchers mapping who ranks for a topic, creator-discovery and partnerships teams, and developers or AI engineers feeding public video metadata into a pipeline.
What YouTube search data is publicly available to scrape?
Everything on a YouTube search results page is visible to a signed-out visitor, and so is everything on a public watch page and a public channel's About tab — titles, view counts, likes, comment counts, descriptions, tags, durations, publish dates, subscriber counts and channel links. What sits outside that surface is anything personalised, owner-only, or deliberately put behind a sign-in button.
| Data Category | Publicly available | Gated behind a Google sign-in or channel ownership |
|---|---|---|
| Video title, watch URL, video ID, duration | ✅ Public | — |
| View count, like count, comment count | ✅ Public | — |
| Full description, tag list, hashtags, thumbnail | ✅ Public | — |
| Exact publish timestamp and search rank | ✅ Public | — |
| Channel name, handle, channel ID, subscriber count | ✅ Public | — |
| Channel About: description, country, joined date, external links | ✅ Public | — |
| Members-only status of a result | ✅ Public flag | The video itself — channel membership |
| A creator's business email | ❌ | Sign-in-gated button on the About tab — not returned, and no field pretends to be one |
| Comment text, transcripts and captions | ❌ | Not returned by this Actor — separate surfaces |
| Watch history and personalised result ranking | ❌ | Google sign-in |
| Audience demographics, impressions, retention | ❌ | YouTube Studio — channel owner only |
YouTube Search Scraper only returns publicly visible data — what any visitor sees. Nothing behind a login wall.
What data can I extract with YouTube Search Scraper?
Rows carry four things: the result's identity and link, its engagement numbers, the channel that published it, and provenance fields recording which query produced the row and when. Thirty-five distinct keys exist across the four row shapes the Actor writes — video results, channel results, playlist results and channel About rows — and keys are omitted rather than nulled when they do not apply to a shape, so read the dataset with .get() rather than assuming a key is present.
| Field Name | Description |
|---|---|
type | Row shape: video, channel, playlist, or error on an uncharged failure row. Classified per result from its own URL and ID shape, not from your type input |
order | The result's real search position, taken from YouTube's own playlist_index. Where YouTube ships no index the stream position is used instead, and the log reports how many rows that affected |
title | Video, channel or playlist title as published |
url | Canonical link — the watch URL for a video, the channel or playlist URL otherwise |
id | Video ID on video rows; channel ID or playlist ID on the other search shapes |
date | Publish time as an absolute ISO 8601 UTC timestamp, e.g. 2026-06-11T15:02:41Z. Read from YouTube's exact epoch, falling back to the YYYYMMDD upload date at midnight UTC. null on channel and playlist result rows |
duration | Runtime as a display string — M:SS, or H:MM:SS past an hour. null on channel and playlist result rows |
viewCount | View count as an integer, not a "1.2M views" string |
likes | Like count as an integer. Comes from the per-video payload, so it needs type: video |
commentsCount | Comment count as an integer |
commentsTurnedOff | true when a fully extracted video carries no comment count at all. null when the run did not fetch deeply enough to know — a missing count on the search-page tier is never reported as "disabled" |
isMembersOnly | true when YouTube's availability is subscriber_only or premium_only. null when YouTube states no availability |
channelName | Publishing channel's display name |
channelUsername | Channel handle in @name form, parsed from the uploader ID or the channel URL |
channelUrl | Channel URL |
channelId | Channel ID (UC…) |
numberOfSubscribers | Subscriber count as an integer, on result rows |
hashtags | Hashtags parsed out of the description, deduplicated case-insensitively with first-seen casing kept |
keywords | The video's own tag list, with YouTube's internal yt:-prefixed entries stripped |
thumbnailUrl | Highest-resolution thumbnail available, chosen by pixel area |
text | Full description, returned whole — no truncation, no appended ellipsis |
input | The exact query string or URL from your input that produced this row |
scrapedAt | Collection time, ISO 8601 UTC |
runId | Apify run ID, stamped on every charged row when the run environment exposes one |
error | Present only on uncharged type: "error" rows: the last message the run saw for that query or URL |
Channel About fields — returned for channel URL inputs only
Pass a channel URL and the row is built from the About page rather than the search page. It carries its own key set, and YouTube's display strings rather than integers.
| Field Name | Description |
|---|---|
description | The channel's About description. A separate key from text, and one of the seven keys that sit outside the default dataset view |
country | Channel country as YouTube states it. null when the channel publishes none — empty means unknown, never guessed |
joined | Join date as YouTube renders it, e.g. Joined Apr 6, 2017 — a display string, not a parsed date |
subscribers | Subscriber count as a display string, e.g. 3.94M subscribers |
views | Lifetime channel views as a display string |
videos | Public video count as a display string |
links | Array of {title, url} objects — the channel's external links, with YouTube's redirect?q= wrapper unwrapped to the real destination and the truncated display text replaced by the true URL |
canonicalChannelUrl | Canonical youtube.com/@handle URL, falling back to YouTube's own canonical value |
inputUrl | The channel URL you supplied |
aboutUrl | The exact …/about URL that was fetched |
🤖 Add-on: Need additional YouTube data?
Companion Scraper-Engine Actors cover the surfaces this one deliberately leaves out. YouTube Video Details Scraper goes deep on a single video, YouTube Playlist Scraper returns a playlist's full contents with publish dates and video stats, YouTube Channel Finder resolves channels from names and handles, and YouTube Community Posts Scraper collects the community tab. Discover creators here, then hand the URLs to whichever of those fits your next step.
How does YouTube Search Scraper differ from the official YouTube Data API?
The YouTube Data API v3 is public, documented, and does support search — so the difference is not capability but what you provision, what you budget, and what a single result row contains when it lands. Every row below is qualitative on purpose: verify the current scopes, terms and quota costs against Google's current published YouTube Data API documentation before committing to either route, because those change and no figure from them is reproduced here.
| Feature | YouTube Data API v3 | YouTube Search Scraper |
|---|---|---|
| Credentials to start | Google Cloud project plus an API key | Apify token only |
| Daily search budget | Search is the expensive call against a daily quota — the constraint search-heavy teams actually hit | Apify platform usage; no Google quota |
| Engagement metrics on a search result | Search returns identifiers and snippets; counts need a second lookup per video | viewCount, likes and commentsCount on the same row as the result |
| Tags and full description | Not part of the search response | keywords and text on the row |
| Channel external links | Not exposed by the search endpoint | links on a channel URL row |
| Ordering you can check | Ordering is the API's own | viewCount and date are re-sorted locally from the real numbers, and the log reports how many rows carried the sort key |
| Write access to your own channel | Supported | Not supported — read-only public data |
Use the official API when you need authenticated operations on your own channel, contractual terms for a shipped product, or fields Google exposes only to an authorised client. Use YouTube Search Scraper when a search-heavy workload would exhaust that daily budget, when you want engagement metrics attached to search results without a second call per video, or when you want to start from a keyword without provisioning anything.
How to use YouTube Search Scraper
YouTube Search Scraper runs on Apify. Start it from the Apify Console, or call it through the Apify API.
- Open YouTube Search Scraper on Apify and click Try for free
- Replace the contents of Search queries or URLs (
queries) with your own — one keyword or one YouTube URL per line. Nothing is strictly required, which is exactly why this step matters: leaving the field untouched runs the built-in default query - Set Max results per query (
maxResults) — 1 to 2000, default5. It is a per-query target, not a run total - Choose your filters: Sort by, Upload date filter, Duration filter and Search type. All four are encoded into YouTube's own
sp=search parameter and sent with the request - Leave Proxy alone unless you have your own residential egress — a video search escalates to Apify Residential on its own if it needs to
- Click Start, then download the dataset as JSON, CSV or Excel, or read it through the Apify API
Short, focused phrases behave like searches. python programming works; a whole sentence usually does not.
How to scale to bulk YouTube search extraction
queries is a list, so one run covers a whole topic set: paste one keyword or URL per line and each is processed in turn, up to five searches and five URLs concurrently. Plain strings and {"url": "…"} objects are both accepted, so output from another tool can usually be fed in unchanged.
maxResults applies per query, not per run — eight keywords at maxResults: 100 is a target of 800 rows. It does not apply to direct URLs, which produce one row each. Every row records the query that produced it in input, so a single dataset fans back out per keyword afterwards.
What can you do with YouTube search data?
- 🔍 An SEO strategist auditing a topic runs twenty keywords at
maxResults: 50and readsorder,viewCountandnumberOfSubscriberstogether to see whether a keyword is won by reach or by relevance before commissioning anything. - 📈 A content analyst tracking a niche sets
uploadDate: "thisWeek"withsortBy: "viewCount"on a weekly schedule, then diffs onidto separate genuinely new uploads from back catalogue resurfacing in the results. - 🤝 A partnerships lead doing creator discovery runs the topic with
type: "channel", collectschannelUrl, then feeds those URLs back into the same Actor to pull each channel'slinks,countryandjoinedfrom its About page. - 🎬 A video producer planning a format test filters on
durationand readskeywordsandtextacross the top-ranking results to see which tag vocabulary and description structure actually surfaces for the topic. - 🤖 An AI engineer building a topic-monitoring agent indexes
title,textandkeywordsinto a vector store keyed oninputandurl, so an agent can answer "what is being published about this topic on YouTube this week" against live search results instead of a stale export.
Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.
How does YouTube Search Scraper handle rate limits and blocking?
No CAPTCHA is solved and no browser is launched. The Actor drives a version-pinned yt-dlp (2026.7.4, fixed in requirements.txt because it is the entire engine) as a subprocess against YouTube's own search URL, and fetches channel About pages over plain HTTP.
What it does handle is YouTube's refusal signature, and the exit that clears it. A response is classified as a bot wall when the engine's output contains sign in to confirm, not a bot, captcha or http error 429 — matched on the message text, not on a status code. On a wall the run stops retrying that exit and steps down its proxy ladder instead of burning attempts on an address YouTube has already refused.
The ladder depends on what you asked for. A video search tries your proxy first when you selected an Apify proxy, then escalates to Apify Residential; with no proxy selected it starts on Residential, because full per-video extraction returns nothing from a datacenter address. Channel and playlist searches run on the container's own IP and only fall back to Residential if that fails. Channel About pages always try the unproxied path first — it is free, and it avoids YouTube's EU cookie interstitial, which serves a page with no data on it at all. Searches get two attempts per rung, About pages three, on a 30-second HTTP timeout. The log names the proxy used for every request, and debug adds a line per request, retry and proxy switch.
When every rung is exhausted for a query, the Actor writes one uncharged type: "error" row carrying the last message it saw, and continues with your remaining queries rather than failing the run.
⬇️ Input
Eight parameters, and none of them is required — which is the most important thing to know before the first run, because the default that fills the gap is a real search that produces real charged rows. Everything else is a filter or a control.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
queries | No | array | One search query or one YouTube URL per line. Anything starting http:// or https:// is routed as a URL; everything else is sent to YouTube search as typed. A watch?v=, /shorts/ or youtu.be/ link returns that single video; any other YouTube URL is treated as a channel and read from its About page. Plain strings and {"url": "…"} objects both accepted. Default ["python programming"]. | ["python programming", "https://www.youtube.com/@Fireship"] |
maxResults | No | integer | Maximum results per search query. Minimum 1, maximum 2000, default 5. Does not apply to direct URLs. The run logs a proxy-traffic projection before it starts, and warns separately above 100 with full video extraction. | 50 |
sortBy | No | string | Result ordering: relevance (default), date, viewCount, rating. Sent to YouTube as a real sort parameter. viewCount and date are additionally re-sorted locally from the returned numbers and timestamps; rating is passed through only. | "viewCount" |
uploadDate | No | string | Restrict to videos uploaded in a window: "" (none, default), lastHour, today, thisWeek, thisMonth, thisYear. Applied as YouTube's own search filter. Videos only. | "thisMonth" |
duration | No | string | Length band: "" (none, default), short (under 4 min), medium (4–20 min), long (over 20 min). Applied as YouTube's own search filter. Dropped, with a log note, when type is channel or playlist. | "medium" |
type | No | string | What kind of result to ask YouTube for: video (default), channel, playlist, movie. Channel and playlist searches are read from the search page only and need no proxy. | "video" |
proxyConfiguration | No | object | Apify Proxy settings. Read when useApifyProxy is true; Apify Residential remains the terminal fallback for video extraction either way. No default. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
debug | No | boolean | Log every request, retry and proxy switch. Default false. | false |
On queries: the field genuinely holds both keywords and URLs in the same list, and the split is a prefix test on http:// / https:// — nothing more. So a bare handle such as @Fireship is not a URL and is searched as a phrase; pass https://www.youtube.com/@Fireship if you want the About page. It also means a playlist URL falls into the channel branch, where the About parse finds no channel metadata and the URL produces an uncharged error row.
On maxResults: a target, not a promise. YouTube decides how deep a query actually goes, and individual videos drop out of extraction because they are private, region-locked or members-only — the engine simply omits them. The run logs the shortfall with YouTube's own message attached instead of padding the dataset, and the gap stays visible in order because order is YouTube's position rather than a renumbering.
On sortBy: relevance sends no sort code at all and leaves YouTube's native ordering intact. rating is passed to YouTube but cannot be verified here, because no rating figure appears anywhere in the payload — the log says exactly that when you select it. YouTube also blends relevance back into its own sort orders, so any ordering is strongest at the top of the set; that is why viewCount and date are re-sorted locally from the real values, with the log reporting how many rows carried the key.
On type: movie is passed to YouTube unchanged, but YouTube rarely returns movie rows for general queries, and no output row is ever labelled movie — results are classified as video, channel or playlist from their own URL and ID shape. Choosing channel or playlist changes the cost profile as much as the content: those rows are read from the search page only, so they carry no viewCount, likes, commentsCount, duration or date.
On proxyConfiguration: the selection is read only when useApifyProxy is true. A configuration supplying custom proxyUrls without that flag is not picked up, and the run behaves as though you had selected nothing — which for a video search means starting directly on Apify Residential.
Two API-only inputs exist that the UI does not show. urls takes an additional array of YouTube URLs, merged with any URLs found in queries, accepting the same string-or-{"url": …} shapes. cookiesPath takes a path to a cookies file passed through to the engine. Neither is in the input schema, neither has a default, and neither is needed for normal use — they are documented here so an API caller is not surprised by them.
Example input
{"queries": ["python programming","fastapi tutorial","https://www.youtube.com/@Fireship"],"maxResults": 50,"sortBy": "viewCount","uploadDate": "thisMonth","duration": "medium","type": "video","proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]},"debug": false}
⬆️ Output
Typed, normalized JSON with stable key names across runs. Rows are written to the dataset as each query finishes, so a stopped run keeps everything collected up to that point. Export as JSON, CSV or Excel, or read the dataset through the Apify API.
The dataset is not rectangular by design: keys that do not apply to a row shape are omitted rather than filled with null or a fabricated 0. A channel result row has no meaningful date or duration; a channel About row has no viewCount, likes or order key at all; only video rows carry likes, keywords and commentsCount. Read with .get(), or branch on type first.
Each saved result row is billed as one row_result event. Error rows are pushed deliberately uncharged — a query that returns nothing must not bill. There is exactly one marker for them, and it is type:
results = [row for row in items if row.get("type") != "error"]
Error rows carry type: "error", an error message and — except in the "no queries at all" case — the input that produced them plus a scrapedAt stamp. They never carry runId, because only charged rows are stamped with it.
The default dataset view surfaces 28 columns. Seven keys the Actor writes sit outside it — id, channelId, runId, description, canonicalChannelUrl, inputUrl and aboutUrl — so read the raw dataset items (for example with iterate_items()) rather than the default view if you need them.
Example output
A video result row:
{"title": "FastAPI in 100 Seconds","type": "video","channelName": "Fireship","date": "2026-06-11T15:02:41Z","text": "FastAPI is a modern Python web framework built on Starlette and Pydantic. Build a production REST API with automatic OpenAPI docs, dependency injection and async request handling.\n\n#python #fastapi #webdev","thumbnailUrl": "https://i.ytimg.com/vi/7t2alSnE2-I/maxresdefault.jpg","order": 1,"input": "fastapi tutorial","hashtags": ["#python", "#fastapi", "#webdev"],"channelId": "UCsBjURrPoezykLs9EqgamOA","channelUrl": "https://www.youtube.com/channel/UCsBjURrPoezykLs9EqgamOA","channelUsername": "@Fireship","numberOfSubscribers": 3940000,"id": "7t2alSnE2-I","url": "https://www.youtube.com/watch?v=7t2alSnE2-I","viewCount": 1284366,"likes": 48219,"commentsCount": 1372,"commentsTurnedOff": false,"duration": "2:41","keywords": ["fastapi", "python", "rest api", "async", "pydantic"],"isMembersOnly": false,"scrapedAt": "2026-07-25T09:14:02Z","runId": "kR8mQzVx2LpNc7Yd4"}
A channel result row from a type: "channel" search — video-only keys are null, not zeroed:
{"title": "Fireship","type": "channel","channelName": "Fireship","date": null,"text": "High-intensity code tutorials to help you build and ship your app faster.","thumbnailUrl": "https://yt3.googleusercontent.com/ytc/AIdro_mQzVx2LpNc7Yd4=s900-c-k-c0x00ffffff-no-rj","order": 2,"input": "web development channels","hashtags": [],"channelId": "UCsBjURrPoezykLs9EqgamOA","channelUrl": "https://www.youtube.com/channel/UCsBjURrPoezykLs9EqgamOA","channelUsername": "@Fireship","numberOfSubscribers": 3940000,"id": "UCsBjURrPoezykLs9EqgamOA","url": "https://www.youtube.com/@Fireship","viewCount": null,"likes": null,"commentsCount": null,"commentsTurnedOff": null,"duration": null,"keywords": [],"isMembersOnly": null,"scrapedAt": "2026-07-25T09:14:05Z","runId": "kR8mQzVx2LpNc7Yd4"}
A channel About row, produced by passing a channel URL — a different key set entirely, with YouTube's own display strings:
{"type": "channel","url": "https://www.youtube.com/@Fireship","input": "https://www.youtube.com/@Fireship","title": "Fireship","channelName": "Fireship","description": "High-intensity code tutorials to help you build and ship your app faster.","joined": "Joined Apr 6, 2017","country": "United States","subscribers": "3.94M subscribers","views": "621,904,113 views","videos": "712 videos","canonicalChannelUrl": "https://www.youtube.com/@Fireship","channelUsername": "@Fireship","channelId": "UCsBjURrPoezykLs9EqgamOA","links": [{ "title": "Website", "url": "https://fireship.io" },{ "title": "GitHub", "url": "https://github.com/fireship-io" }],"inputUrl": "https://www.youtube.com/@Fireship","aboutUrl": "https://www.youtube.com/@Fireship/about","scrapedAt": "2026-07-25T09:14:07Z","runId": "kR8mQzVx2LpNc7Yd4"}
An uncharged error row for a query that returned nothing:
{"type": "error","input": "zqxjv nonexistent phrase","error": "no results returned","scrapedAt": "2026-07-25T09:14:09Z"}
How does it work?
YouTube's search filters all live in a single sp= parameter on youtube.com/results — a base64 protobuf encoding sort order, upload window, result type and video length. YouTube Search Scraper builds that parameter from your four filter inputs and requests the same URL a browser would, so filtering happens server-side at YouTube: nothing is fetched and then discarded, and you never pay to collect rows a filter throws away afterwards. The one post-fetch step is a re-order, applied when you sort by viewCount or date, because YouTube blends relevance back into its own sort orders — and that step reorders rows, it never removes them.
Results come back as structured JSON from a version-pinned engine rather than as scraped markup, then get normalised onto the row shapes above, deduplicated within each query, and stamped with order, input, scrapedAt and runId. Channel About pages are parsed out of the page's own ytInitialData payload. Because the Actor reads YouTube's structured data rather than rendered HTML, a YouTube front-end redesign does not change your field names. Only publicly visible data is collected — no account, cookie or session is used.
Integrations
YouTube Search Scraper is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset — schedules, webhooks, no-code platforms and agent frameworks included.
Calling YouTube Search Scraper from Python
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/youtube-search-scraper").call(run_input={"queries": ["fastapi tutorial", "python programming"],"maxResults": 50,"sortBy": "viewCount","uploadDate": "thisMonth","type": "video",})for row in client.dataset(run["defaultDatasetId"]).iterate_items():if row.get("type") == "error":continueprint(row["order"], row["input"], row.get("viewCount"), row["title"], row["url"])
Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Skipping rows whose type is error leaves you with results only.
No-code tools (n8n, Make, LangChain)
In n8n, use the Apify node — or an HTTP Request node pointed at the Apify run endpoint with your token — and pass the same JSON input shown above; a Filter node on type drops the error rows. In Make, the Apify module supports run-and-wait, so a weekly keyword sweep can feed a Google Sheets or Airtable step directly. In LangChain, wrap the run endpoint as a tool and hand the agent queries and maxResults; the response is already typed JSON, so no output parser is needed. Apify's own schedules and webhooks cover the recurring case with no external tool at all.
Is it legal to scrape YouTube search results?
Collecting publicly listed video metadata is broadly treated as permissible where no authentication is bypassed, and YouTube Search Scraper reads only signed-out search results and public About pages — no account, no cookie, no session.
What comes back is content metadata, not personal data: video titles, view counts, like counts, durations, tags, publish timestamps, channel names and channel statistics. No viewer information, no comment authors, no watch history and no contact details are collected, so the personal-data regimes that govern a profile scraper do not attach to this output the way they would there. The obligations that do apply are contractual — YouTube's terms of service — and copyright-related, if you republish titles, descriptions or thumbnails as your own catalogue.
One nuance is worth stating plainly: a channel is not always an organisation. An individual creator's channel identifies a person, so channelName, channelUsername, country and links on that row can be personal data even though the same keys on a broadcaster's or a company's channel row are not.
Consult legal counsel if your use case involves bulk storage of individual creators' details, or redistribution of collected metadata at scale.
❓ Frequently asked questions
What YouTube search fields does YouTube Search Scraper return?
The five most used are title, url, viewCount, channelName and date. Beyond those, video rows carry likes, commentsCount, keywords, text, duration, order, hashtags, thumbnailUrl and the full channel block — 35 distinct keys exist across the four row shapes, all listed in the data fields tables above.
Does YouTube Search Scraper require a YouTube account or login?
No. It runs signed out end to end — no YouTube account, no Google login, no Google Cloud project, no API key. The only credential you need is your Apify token.
How many results can I extract in one run?
maxResults accepts 1 to 2000 and applies per query, so a run's ceiling is your query count multiplied by that number; direct URLs produce one row each regardless. That is the Actor's cap. The real ceiling is YouTube's: a search only goes as deep as YouTube will page it, and individual videos drop out when they are private, region-locked or members-only. The run logs the shortfall with YouTube's own message rather than padding the dataset, and the gap stays visible in order.
What happens when a query returns zero results?
The query still produces exactly one row — an uncharged row with type: "error", the input that produced it, a scrapedAt stamp and an error message carrying the last thing the run saw — and the run continues with your remaining queries. That message distinguishes a genuinely empty search from an upstream refusal, since a detected bot wall is reported verbatim. Filter with row.get("type") != "error" to keep results only, or read the error rows deliberately to see which queries under-delivered and why.
Can I search multiple YouTube keywords at once?
Yes. queries is a list — one keyword or URL per line, processed up to five at a time. Keywords and URLs can be mixed freely in the same list. Deduplication happens within each query's result set, keyed on type plus ID, so a video that legitimately ranks for two of your keywords appears once under each, and input tells you which is which.
Are view counts and publish dates absolute numbers or YouTube's relative strings?
Absolute, on result rows. viewCount, likes, commentsCount and numberOfSubscribers are integers, not "1.2M views" strings, and date is a full ISO 8601 UTC timestamp rather than "2 years ago" — so you can sort, average and diff without parsing. The exception is the channel About row: subscribers, views, videos and joined are YouTube's own display strings there, because the About page publishes them abbreviated and nothing is invented to fill the gap.
Why does a run return channel or playlist rows when I asked for videos?
Because every result is classified from its own URL and ID shape rather than from your type input, and YouTube's search results genuinely mix entities. Branch on the type key: it is video, channel or playlist on result rows, and error on uncharged failure rows. One consequence worth noting — type: "movie" is passed through to YouTube, but no row is ever labelled movie; movie results arrive typed video.
Does YouTube Search Scraper work with Claude, ChatGPT and other AI agent tools?
Yes. It is callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step. Give the agent queries and maxResults, and have it skip any row whose type is error.
Are results identical between two runs of the same query?
No, and that is YouTube rather than the Actor. Two identical searches minutes apart can return partly different videos in a partly different order, so a run-to-run difference is not evidence that a filter did or did not apply. What is stable is the schema: the same key names and types every run, with scrapedAt and input stamped on each row so any snapshot carries its own provenance.
How does YouTube Search Scraper compare to other YouTube search scrapers?
Checked on the Apify Store on 25 July 2026. apidojo/youtube-scraper-api is the most used of the three and broader in surface — it documents startUrls, youtubeHandles, keywords, getTrending, gl/hl locale parameters and a customMapFunction, covering trending, Shorts and live streams as well as search; its documented duration filter offers short and long, where this Actor also exposes a medium band. khadinakbar/youtube-search-scraper documents a required searchQueries input, a features filter, a raw sp token override and a paid SerpApi fallback engine; its published output example returns publishedTimeText as a relative string such as "2 years ago" alongside a parsed viewCount. code-node-tools/youtube-scraper is URL-driven rather than query-driven — inputType with videoUrls, channelUrls or playlistUrls — and its own published output example shows viewCount as "1.5B views" and uploadDate as "13 years ago", i.e. display strings rather than numbers; its README lists search-result scraping as "coming soon". None of the three listings documents uncharged rows for empty queries, a local re-sort of results from the returned view counts and timestamps, or channel About external-link extraction — which is not the same as saying those are unsupported.
What happens when YouTube changes its layout or anti-bot system?
The scraper is maintained, and because it reads YouTube's structured data rather than rendered markup, a visual redesign generally does not affect it. The two parts that do move are handled deliberately: the extraction engine is version-pinned so an upstream change cannot silently alter your output, and the filter parameter is built from YouTube's own wire format rather than copied from a URL. Your field names and types do not change on your end.
Which YouTube fields work best for AI training data and RAG indexing?
For RAG indexing: text, title and keywords carry the most descriptive content per row and chunk cleanly, with url as the citation anchor and input as a natural partition key. For training data: viewCount, likes, commentsCount, duration, order and date are the most structurally consistent fields across records, arriving as integers, a fixed-format duration string and an ISO timestamp rather than display text. All values return as typed primitives, arrays of strings, or arrays of flat {title, url} objects — no normalization pass is needed before indexing, though a .get() is, because inapplicable keys are omitted rather than nulled.
🔗 Related scrapers
| Scraper Name | What it extracts |
|---|---|
| YouTube Video Details Scraper | Deep metadata for a single YouTube video |
| YouTube Playlist Scraper | A playlist's full contents with publish dates and video stats |
| YouTube Channel Finder | Channels resolved from names and handles |
| YouTube Community Posts Scraper | Posts from a channel's community tab |
| YouTube Shorts Scraper | Short-form videos with creator lead enrichment |
| Facebook Video Search Scraper | Video search results for the same keyword on Facebook |
💬 Your feedback
Found a bug, or missing a field YouTube exposes that is not in the output? Open an issue on the Actor's Issues tab. Reports that include the exact query and the input JSON you ran are the fastest to reproduce and fix.