YouTube Search Scraper — All Filters, Video & Channel Data
Pricing
$0.80 / 1,000 per search result returneds
YouTube Search Scraper — All Filters, Video & Channel Data
Scrape YouTube search results by keyword with every filter YouTube offers: 4 sort orders, 6 upload windows (from last hour), 6 result types, 4 durations, 11 feature flags. Each row: video ID, title, description snippet, channel name/ID/handle, views, duration, thumbnails, badges, chapters.
Pricing
$0.80 / 1,000 per search result returneds
Rating
0.0
(0)
Developer
Scrapers Delight
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 days ago
Last modified
Categories
Share
🔎 YouTube Search Scraper — every filter, every field in one request
Scrape YouTube search results by keyword and get back the whole row YouTube's own search response contains: video ID, title, description snippet, channel name and channel ID and @handle, view count, duration, thumbnail ladder, badges, and per-video chapters — plus the complete filter surface (4 sort orders × 6 upload windows × 6 result types × 4 durations × 11 feature flags).
No API key. No login. No cookies. No browser. One HTTP request per page of results.
🚀 What does YouTube Search Scraper do?
You give it keywords. It runs them through YouTube's own internal search endpoint, walks the continuation chain, de-duplicates, and writes one row per result to your dataset.
- 🎬 Videos — the full result row, in YouTube's own ranking order (
searchPosition) - 📺 Channels —
resultType: channelreturns channel rows with ID, @handle, subscribers, bio - 📚 Playlists — ID, owner, video count, preview titles, first video
- 🍿 Movies — genre, release year, cast, director, runtime
- ⚡ Shorts — including the uploader's channel ID, which YouTube does not put in a Shorts row at all (recovered from the reel endpoint — see The wedges, below)
- 🔴 Live streams — with the live viewer count, correctly separated from view count
- 🧭 Chapters — title, timestamp and
startSeconds, straight off the search response, without a second request per video
📊 Every field on a video row
| Field | What it is | Fill on the 600-row proof run |
|---|---|---|
type | video / short / channel / playlist / movie | 100.0% |
videoId | YouTube's 11-char ID | 100.0% |
url | https://www.youtube.com/watch?v=… | 100.0% |
title | Video title | 100.0% |
titleAccessibilityLabel | Spoken title incl. duration | 100.0% |
descriptionSnippet | The search snippet, query terms included | 99.7% |
descriptionSnippetHoverText | What the snippet was taken from | 99.7% |
channelName | Uploader name | 100.0% |
channelId | UC… channel ID | 100.0% |
channelHandle | @handle | 99.8% |
channelUrl | Canonical channel URL | 100.0% |
channelVerified | Verified badge present | 100.0% (true on 273) |
channelBadges | Verified / Official Artist Channel | 45.7% |
channelAvatarUrl | Largest avatar | 100.0% |
channelAvatarThumbnails | Full avatar ladder {url,width,height} | 100.0% |
collaborators | Every co-uploader of a collab video, with ID + handle + subscribers | 0.3% |
viewCountText | "3,315,126 views" | 100.0% |
viewCount | Integer 3315126 ("No views" → 0) | 99.8% |
shortViewCountText | "3.3M views" | 100.0% |
liveViewerCountText | "15 watching" — live rows only | 0.2% |
liveViewerCount | Integer live viewers | 0.2% |
publishedTimeText | "6 years ago" / "Streamed 1 day ago" | 99.8% |
publishedTimeApprox | ISO date derived from that — approximate, see limits | 99.8% |
durationText | "18:37" | 99.8% |
durationSeconds | Integer 1117 | 99.8% |
durationAccessibilityLabel | "18 minutes, 37 seconds" | 99.8% |
isLive / isShort / isUpcoming | Row-kind flags | 100.0% |
scheduledStartTime | ISO start of an upcoming premiere | 0.0% on this run |
badges | New / CC / 4K / LIVE labels | 48.7% |
hasCaptions | CC badge present | 100.0% (true on 76) |
is4K | 4K badge present | 100.0% (true on 244) |
thumbnails | Full ladder, 360×202 + 720×404 | 100.0% |
thumbnailUrl | Largest thumbnail | 100.0% |
movingThumbnailUrl | Animated hover preview (.webp) | 77.5% |
chapters | [{title, time, startSeconds}] | 31.7% (190 rows, 1,811 chapters) |
chaptersCount | Integer | 100.0% |
playerParams | YouTube's player params for the row | 100.0% |
searchVideoResultEntityKey | Stable per-result tracking key | 100.0% |
inlinePlaybackUrl | Inline-preview watch URL | 97.0% |
searchQuery | Which of your keywords produced this row | 100.0% |
searchPage | Continuation page index | 100.0% |
searchPosition | Rank inside the result set — YouTube's own ordering | 100.0% |
estimatedResults | YouTube's estimate for the whole query | 100.0% |
scrapedAt | ISO timestamp of the scrape | 100.0% |
46 fields on a video row; 28 of them at 100.0% fill across the 600-row proof run.
Other row types (measured)
| Type | Rows measured | Fields | Notable |
|---|---|---|---|
channel | 80 | 23 | channelId 100%, @handle 100%, subscriberCount 98%, descriptionSnippet 90% |
playlist | 40 | 21 | playlistId, owner ID + handle, videoCount, previewVideoTitles, firstVideoId — all 100% |
movie | 19 | 32 | releaseYear 100%, genre 100%, cast 100%, director 89% |
short | 51 | 46 | videoId 100%, viewCount 100%, channelId 100% |
🎛️ Every filter YouTube has — not a hand-picked subset
Most YouTube search scrapers ship a fixed lookup table of pre-baked filter strings, so you get whatever combinations somebody typed out by hand. This actor encodes YouTube's search protobuf properly, so the entire cross-product is reachable:
| Filter | Values |
|---|---|
sortBy | relevance · uploadDate · viewCount · rating |
uploadDate | any · lastHour · today · thisWeek · thisMonth · thisYear |
resultType | video (default) · any · shorts · channel · playlist · movie |
duration | any · under3min · from3to20min · over20min |
features | live · fourK · hd · subtitles · creativeCommons · threeSixty · vr180 · threeD · hdr · location · purchased |
And it checks itself. Every search response carries YouTube's own filter chips with their official encodings. On the first page of every run the actor decodes those chips and compares them to its own encoder. If YouTube ever renumbers an enum, the actor adopts YouTube's value, logs the drift, and re-runs the query — instead of silently handing you unfiltered results.
Measured on the proof runs: 24 of 24 encodings byte-identical to YouTube's own chips
(type=video → EgIQAQ==, duration>20m → EgIYAg==, VR180 → EgPQAQE=, …), and the actor
logged encoder confirmed against 23 live filter chips on every platform run.
🧩 The wedges — what this does that the search response does not hand you
- Shorts get a channel. YouTube's
shortsLockupViewModelhas a title, a view count and a thumbnail, and nothing else — no channel name, no ID, no handle. The uploader'sUC…ID is embedded in the reel endpoint's params protobuf, so it is recovered there. Verified against the videos' own watch pages: 4 of 4 exact matches, then 51 of 51 Shorts rows on the platform. - Collaboration videos keep their channel. When two creators co-upload, YouTube replaces the
byline link with a "Collaborators" dialog — a parser that only reads the byline loses
channelIdandchannelHandleon those rows. Here they are read from the dialog, and every collaborator ships incollaborators[]with ID, handle and subscriber count. - Chapters without a second request. 1,811 chapters across 190 of 600 rows, from the same response — no extra fetch, no extra latency, no extra charge.
"No views"is0, not null. A brand-new upload has a real, meaningful zero."15 watching"is never mis-read as a view count. It lands inliveViewerCount.
📥 Input
{"searchQueries": ["plumber near me", "best crm software"],"resultType": "video","sortBy": "relevance","uploadDate": "any","duration": "any","features": [],"maxResultsPerQuery": 120,"maxResults": 0,"language": "en","region": "US","deduplicateAcrossQueries": false,"proxyConfiguration": { "useApifyProxy": true }}
| Input | Default | Notes |
|---|---|---|
searchQueries | ["web scraping"] | One per line. Volume comes from many queries, not one deep one — see the ceiling below. |
resultType | video | video returns only videos. any reproduces youtube.com, Shorts shelf included — you pay for those rows too. |
sortBy / uploadDate / duration / features | unfiltered | Server-side, combinable. |
maxResultsPerQuery | 200 | Per-query stop. |
maxResults | 0 (unlimited) | Overall stop across every query. |
language / region | en / US | hl / gl. YouTube ranks per country — this is how you scrape the German result set for the same keyword. |
deduplicateAcrossQueries | false | Duplicates inside a query are always dropped before billing. This also drops a result that already appeared under a different keyword. |
proxyConfiguration | Apify Proxy auto | Verified HTTP 200 through auto, through residential, and from a home IP. |
💵 Pricing
Pay per event — one event, nothing else. No per-run start fee, no auto-events.
| Event | Price |
|---|---|
search-result-scraped — per search result returned | $0.0008 = $0.80 / 1,000 results |
Billing runs through Actor.pushData(rows, 'search-result-scraped'), which is budget-aware:
delivered == billed. If you set a charge cap, you are never handed rows you did not pay for, and
never billed for rows you did not get. Duplicates from YouTube's continuation chain are removed
before they are pushed, so you do not pay for repeats.
Measured on the platform: the 600-row run reported chargedEventCounts: {"search-result-scraped": 600}
against a dataset of exactly 600 items.
Honest comparison — do not take a blanket "cheapest" claim from anybody, including us:
- vs
api-ninja/youtube-search-scraperfree tier ($0.0025/result + a per-run fee): we are 3.1× cheaper per result and charge no run fee. - vs the same actor's paid tiers ($0.00025/result on Bronze and above): they are cheaper. If you are on a paid Apify plan and only want their field set, they cost you less per row. What you get here instead is the wider row, the complete filter cross-product, and one flat rate.
- vs
khadinakbar/youtube-search-scraper($0.003/result): we are 3.75× cheaper.
📈 Measured performance (real runs on Apify, 2026-09-02)
| Run | Config | Result |
|---|---|---|
| Proof run | 5 queries × 120, type=video, Apify Proxy auto | 600 rows, 600 unique, 69.5s, 35 requests, 100% success, 0 blocks |
| Depth run | 2 queries, maxResultsPerQuery: 1000 | 820 rows, 51 requests, 100% success — both queries exhausted their continuation chain |
| Mixed | resultType: any | 80 rows: 51 Shorts + 28 videos + 1 channel |
| Channels / Playlists / Movies / Shorts | one run each | 80 / 40 / 19 / 40 rows, all types parsed |
| Filter proof | sortBy: uploadDate + uploadDate: lastHour + hd | 40 rows, freshest "21 minutes ago" |
| Health (prefill input) ×3 | SUCCEEDED in 7.3s / 30.4s / 6.0s, 50 rows each | |
Empty input {} ×2 | SUCCEEDED in 13.3s / 23.9s, 200 rows each — never fails on empty input |
Sustained-load check: 86 consecutive InnerTube requests across the two deep runs, through Apify Proxy, zero non-200 responses and zero 403/429. Requests are logged with a running success rate, so you can see it yourself in any run.
⚠️ Honest limits — read these before you buy
- YouTube caps how deep one query goes. Measured to exhaustion on the platform:
"plumber"ran dry at 473 unique results / 31 pages,"best crm software"at 347 / 20 pages. Then the continuation token goes null and the query ends (the log saysno further pages). This is YouTube's ceiling, not this actor's — every YouTube search scraper hits it. Plan for volume by running many queries and filter variants, not one enormous one. - YouTube's continuation chain repeats itself. Measured 151 duplicates in 971 raw rows (15.6%)
on the depth run. They are dropped by ID before
pushData, so you are never billed for them — but it is why "20 results per page × 30 pages" does not equal 600 unique rows. - The publish date is relative, not absolute. Search returns
"6 years ago", never an ISO date.publishedTimeApproxis that string resolved against the scrape time, so it is exact to YouTube's own granularity and no better. It is namedApproxon purpose. An exact timestamp needs a second request per video, which this actor deliberately does not make. - Live rows have no duration and no publish date. That is YouTube, not a bug — a live row
carries
liveViewerCountinstead. Verified: 20 of 20 rows under thelivefilter. - Shorts rows are thin by nature. YouTube gives a Shorts result a title, a view count and a
thumbnail. This actor adds
channelId(100% of 51 rows). It cannot give you the channel name or @handle for a Short, because they are not in the payload. videoCountText/videoCounton channel rows measured 0/80. YouTube currently puts the subscriber count in that slot instead. The parser reads those fields by content rather than by name, so they will populate again if YouTube restores them — today they are empty, and this README says so rather than pretending.- No per-video enrichment. Like count, comment count, subscriber count on a video row, full
description, keywords and tags are not in the search response. They need a separate
/playeror/nextcall per video. This actor is one request per page, on purpose — it does not claim fields it cannot see. - Sustained very-high volume is unproven. 86 consecutive requests came back clean. Nobody has
run this at tens of thousands of requests an hour. If you do and YouTube starts refusing, the run
exits cleanly with a "blocked" status message — never as "no results found", and never as a
FAILED run. Switch
proxyConfigurationtoRESIDENTIAL, or bring your own proxy. - Residential proxy costs bandwidth. A result is roughly 36 KB of response. On Apify Proxy
auto(the default) bandwidth is not billed. On residential at ~$8/GB it is about $0.29 per 1,000 rows — still comfortably inside the price, but do not switch it on out of habit.
⚖️ Legal & fair use
This actor reads YouTube's public search results — the same rows any visitor sees, with no login, no account, no cookies and no CAPTCHA solving. It collects no personal data: channels are public business/creator profiles, and no viewer or commenter information is touched.
https://www.youtube.com/robots.txt lists, under User-agent: *:
Disallow: /resultsDisallow: /youtubei/
That is quoted here verbatim so you can make your own call. robots.txt is a crawling convention, not law, and it applies to indexing crawlers; you are responsible for your own use of the output and for compliance with YouTube's Terms of Service in your jurisdiction. Keep request volume reasonable, do not republish YouTube content wholesale, and use the data for research, analysis and internal tooling.
❓ FAQ
How many results can I get for one keyword?
Measured: 473 for "plumber", 347 for "best crm software", then YouTube stops paginating. Use
more keywords and more filter variants (each variant returns a genuinely different slice).
Do I pay for duplicate rows?
No. Duplicates within a query are dropped before they are pushed or charged. Set
deduplicateAcrossQueries: true if you also want a result de-duplicated across different keywords.
Can I get the exact upload date?
Not from search — YouTube only exposes a relative string there. publishedTimeApprox resolves it
against the scrape time and is labelled approximate for that reason.
Can I get like counts, comment counts, or subscriber counts on a video row? No, and no honest YouTube search scraper can do it in one request — they are not in the search response. Pair this with a video-detail actor if you need them.
Does it get YouTube Shorts?
Yes. resultType: shorts returns only Shorts; resultType: any mixes them in as youtube.com does.
Each Short comes with its videoId, view count, thumbnails and the uploader's channelId.
Can I search a specific country or language?
Yes — region (gl) and language (hl). YouTube ranks per country, so region: "DE" gives you
the German result set for the same keyword.
Can I sort by upload date or rating?
Yes. sortBy: "uploadDate" and sortBy: "rating" are encoded straight into YouTube's search
protobuf. So is uploadDate: "lastHour" — verified live returning videos 21 minutes old.
Do I need a proxy?
The default (Apify Proxy auto) is enough and was verified at 100% success across 86 consecutive
requests. Residential is there as an opt-in for very large jobs.
Do I need a YouTube API key or an account? No. No key, no login, no cookies, no quota.
What happens if YouTube blocks the run? It exits cleanly with an explicit "blocked" status message telling you how many requests were refused, and suggests residential proxies. It never reports a block as "no results found", and it never ends FAILED for a transport problem.
What happens if YouTube changes its response shape? If results come back but nothing can be parsed, the run fails loudly rather than quietly handing you an empty dataset. If a filter enum is renumbered, the actor detects it against YouTube's own filter chips and corrects itself mid-run.
Is the result order YouTube's real ranking?
Yes. searchPosition preserves the order YouTube returned, per query, after de-duplication.
What is the cheapest way to run this?
Leave resultType on video (so you are not billed for the Shorts shelf), keep
proxyConfiguration on auto, and set maxResultsPerQuery to what you actually need.
🔬 How this was verified
Nothing in this README is estimated. Every number came from a run you can reproduce:
- The parser was validated offline first against captured live responses — 44 rows on page 1, all five result shapes, the live-row absence trap, and 24/24 filter encodings against YouTube's chips.
- Then on the Apify platform, build
0.1.2: 12 runs, all SUCCEEDED, covering every result type, every filter axis, the depth ceiling, the health-test input and empty input. - Fill percentages are computed over the 600-row proof run, not a sample.