# YouTube Metadata Scraper By Duration, Date & Video Type (`simpleapi/youtube-metadata-scraper`) Actor

YouTube Metadata Scraper extracts detailed metadata from YouTube videos, including titles, descriptions, tags, thumbnails, durations, upload dates, and engagement stats. Ideal for SEO research, content analysis, trend tracking, and automating structured YouTube data collection

- **URL**: https://apify.com/simpleapi/youtube-metadata-scraper.md
- **Developed by:** [SimpleAPI](https://apify.com/simpleapi) (community)
- **Categories:** Automation, Lead generation, Social media
- **Stats:** 11 total users, 1 monthly users, 96.6% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.99 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### YouTube Search Scraper — Duration, Date Range, Shorts & Metadata

YouTube Search Scraper runs a keyword through YouTube's own search — filtered by video length band, upload-date bucket, an exact `after:`/`before:` date range and Videos/Shorts/Movies type, the same way YouTube's live filter bar does — then loads every matching video's own watch page for a full record: title, description, tags, category, thumbnails, exact view and like counts, the real channel `@handle`, top-level comments and caption transcripts. It's built for content researchers, video-trend analysts, licensing/footage teams and AI pipelines that need a precisely filtered, structured slice of YouTube rather than a raw keyword dump. Specific video URLs can also be enriched directly, bypassing search entirely.

***

### What is YouTube Search Scraper?

YouTube Search Scraper searches YouTube the way a logged-out visitor would, then opens each matched video's own watch page to pull its full metadata, comments and transcript. No YouTube account, cookie or Data API key is required anywhere in the actor — every request runs against YouTube's public search and watch pages through Apify Proxy. Output is one structured JSON row per video, ready for the Apify dataset's JSON, CSV, Excel, HTML table or XML export.

- 🎯 **Keyword or direct-URL discovery** — search phrases run through YouTube search with your filters attached, or supply watch links, `youtu.be` links, `/shorts/`, `/live/`, `/embed/` links or bare 11-character video IDs to skip search and enrich known videos directly.
- 🎚️ **Server-side YouTube filters** — video length band, upload-date bucket, Videos/Shorts/Movies type, and nine feature flags (HD, 4K, Subtitles/CC, Creative Commons, Live, 360°, 3D, HDR, VR180) are encoded into YouTube's own `sp=` filter token, so YouTube filters the result set before anything is downloaded.
- 📅 **Exact date range** — `publishedAfter`/`publishedBefore` are sent as real `after:`/`before:` search operators, giving a precise window the upload-date bucket alone cannot reach.
- 🧮 **Post-download filters** — minimum views, minimum likes, an exact second-level duration window, and title include/exclude run against the finished record; rejected videos are never saved.
- 📺 **Full video and channel metadata** — exact view/like counts, channel ID, URL, real `@handle`, display name, subscriber text, verification badge, category, tags, thumbnails and outbound description links.
- 💬📝 **Comments and transcripts** — top-level comments up to your limit, plus caption text (plain or timed segments), with duplicate tracks removed and a language cap.

***

### What data can I extract with YouTube Search Scraper?

Every video row carries identity, engagement, channel, filter-receipt and enrichment fields, all read from the video's own watch page (or the search result card, for the filter-receipt group).

| Field | Example Value | Use Case |
|---|---|---|
| `id` | `"kX8XKlNRVYY"` | YouTube's 11-character video ID, a stable join key |
| `url` | `"https://www.youtube.com/watch?v=kX8XKlNRVYY"` | Canonical watch link |
| `title` | `"8 Hours of Relaxing 4K Drone Footage"` | Headline text for reports and dedupe |
| `description` | `"Filmed across three continents..."` | Full video description text |
| `lengthInSeconds` | `28800` | Exact duration, straight from the video page |
| `uploadDatetime` | `"2024-02-11T14:00:09-08:00"` | ISO-8601 publish date with offset |
| `publish_date_since` | `"1 year ago"` | Relative publish text as YouTube displays it |
| `category` | `"Travel & Events"` | YouTube's assigned category |
| `tags` | `["drone", "4k", "nature"]` | Creator keyword tags |
| `thumbnail` | `"https://i.ytimg.com/vi/kX8XKlNRVYY/hqdefault.jpg"` | Standard thumbnail URL, built from the video ID |
| `thumbnails_urls` | `["https://i.ytimg.com/vi/.../default.jpg", "..."]` | Every signed thumbnail size YouTube served |
| `channelID` | `"UCxxxxxxxxxxxxxxxxxxxxxx"` | Channel's stable `UC…` ID |
| `channelURL` | `"https://www.youtube.com/channel/UCxxxx"` | Canonical `/channel/UC…` URL |
| `channelUsername` | `"@RelaxationFilm"` | Real `@handle`, read from the page's owner profile URL |
| `channelDisplayName` | `"Relaxation Film"` | Channel display name |
| `channelSubscribers` | `"1.2M"` | Abbreviated subscriber text as YouTube publishes it |
| `author_avatar_url` | `"https://yt3.ggpht.com/..."` | Largest available channel avatar URL |
| `is_verified` | `true` | Channel carries a verified or official-artist badge |
| `viewCount` | `1842991` | Exact view count |
| `likeCount` | `21044` | Exact like count |
| `commentCount` | `1200` | Comment total as displayed on the page |
| `is_live_now` | `false` | Currently broadcasting live |
| `description_links` | `["https://instagram.com/relaxationfilm"]` | Outbound description links, redirect-unwrapped |
| `is_comments_off` | `false` | Comments disabled on this video (`null` if undetermined) |
| `is_members_only` | `false` | Members-only video |
| `is_paid_content` | `false` | Paid or purchase-gated content |
| `transcripts` | `[{"language": "EN", "content": "..."}]` | Caption tracks, duplicates removed |
| `comments` | `[{"id": "Ugz...", "text": "..."}]` | Top-level comments, up to `max_comments` |
| `isShort` | `false` | Served as a Short in the search results (`null` for direct URLs) |
| `searchResultBadges` | `["4K"]` | Badges YouTube printed on the search card |
| `foundViaKeyword` | `"drone footage"` | The search phrase that surfaced this video |
| `searchResultRank` | `3` | 1-based position in that phrase's filtered result set |
| `searchResultPage` | `1` | Which search page the video came from |
| `searchFilterToken` | `"EgYQARgCcAE="` | The literal `sp=` token used for this row |
| `searchQuerySent` | `"drone footage after:2024-01-01 before:2024-12-31"` | Exact query string sent, including date operators |
| `appliedFilters` | `["Videos", "Over 20 minutes", "4K", "Relevance"]` | Filter chips YouTube echoed back as selected |
| `channelHandleUrl` | `"https://www.youtube.com/@RelaxationFilm"` | `/@handle` URL built from the real handle |
| `playabilityStatus` | `"OK"` | YouTube's playability status for the fetched page — always `OK` on saved rows |
| `scrapedAt` | `"2026-07-30T09:14:02Z"` | UTC timestamp of the fetch |

#### 💬 Engagement, comments and transcripts

`viewCount` and `likeCount` are exact numbers read directly from the video page, not abbreviated text. `comments[]` (`id`, `text`, `author`, `authorChannelId`, `likeCount`, `publishedTime`, `replyCount`) carries up to `max_comments` top-level comments per video. `transcripts[]` (`language`, `content`) carries caption text — plain text or `{start, dur, text}` timed segments depending on `transcript_format` — with byte-identical creator/auto-generated duplicates removed before the language cap is applied. Analysts use this pair to build engagement-plus-content datasets — for example, ranking `commentCount` against transcript content for a topic without opening a single video by hand.

#### 🎚️ Filter-receipt and targeting fields

`foundViaKeyword`, `searchResultRank`, `searchResultPage`, `searchFilterToken`, `searchQuerySent` and `appliedFilters` document exactly how each row was found and which filters YouTube actually applied — useful for auditing a run or re-issuing the identical search later. `isShort` and `searchResultBadges` (`4K`, `CC`, `LIVE`, `New`, etc.) come straight off the search card. All six are empty, `null` or `""` for videos supplied through `urls`, since those bypass search entirely.

***

### How does YouTube Search Scraper differ from the official YouTube Data API v3?

YouTube's own YouTube Data API v3 is a real, self-serve public API, but its per-project quota is the hardest limit on bulk work: Google's published quota documentation sets a default of 10,000 units per day, and a single `search.list` call — the endpoint that filters by keyword — costs 100 units regardless of how many fields you request, so a default project caps out at roughly 100 search calls a day before every subsequent request fails with a quota error. This actor runs on Apify's infrastructure instead, so a run is bounded by your `max_results_per_keyword` and `maxSearchPages` settings, not a shared daily unit budget.

| Feature | YouTube Data API v3 | This Actor |
|---|---|---|
| Daily request budget | 10,000 units/day by default; `search.list` costs 100 units per call | No shared daily quota — bounded by your own `max_results_per_keyword` / `maxSearchPages` |
| Duration filtering | `videoDuration` enum: `short` / `medium` / `long` only | Six bands matching YouTube's current UI (`under_3`, `3_20`, `over_20`) plus two legacy bands (`under_4`, `4_20`), and an exact second-level `minDurationSeconds`/`maxDurationSeconds` window |
| Exact date range | `publishedAfter`/`publishedBefore` supported on `search.list` | Same operators, applied identically as `after:`/`before:` query terms |
| Caption/transcript access | `captions.download` requires OAuth authorization from the video's own channel owner — third-party video transcripts cannot be downloaded through the API at all | Downloads public caption tracks for any video, creator-uploaded and/or auto-generated, no OAuth |
| Comments | `commentThreads.list` returns top-level comments without OAuth | Comments attached automatically per matched video, no separate call |
| Channel handle | `channels.list` with the `forHandle` parameter | `channelUsername`/`channelHandleUrl` included on every row automatically |
| Authentication | Requires a Google Cloud project and API key | Runs on your existing Apify account |

#### Why can't the official API fetch a video's transcript?

`captions.download`, the only endpoint that returns actual caption text, requires the request to be authorized via OAuth by the channel that owns the video — Google's API reference documents this as a requirement, not an option. In practice that means the official API can only download captions for videos on channels you personally control; it cannot pull a transcript for someone else's video no matter how public that video is. This actor sidesteps the restriction entirely by reading the public caption track URLs the same way a browser does when captions are switched on, which works for any video that exposes them, regardless of who owns the channel.

The official API is the right tool when you already run a small number of your own channels and want OAuth-scoped analytics or caption management. This actor is the fit when you need filtered discovery across other people's videos, or transcripts you don't have upload rights to.

***

### How to use data extracted from YouTube?

#### 🔬 Content researchers and video-trend analysts

Set `keywords` to your topic, a `videoDuration` band (`over_20` for long-form, `under_3` for short-form), and `rankBy: "view_count"` to surface the highest-viewed matches first. Export `title`, `viewCount`, `likeCount`, `channelDisplayName` and `tags` to map which channels and formats dominate a topic, without opening a single search page by hand.

#### 🎬 Licensing and footage-sourcing teams

Combine `videoFeatures: ["creative_commons", "4k"]` with a `videoDuration` band to surface only reusable, high-resolution footage. `description_links` and `channelHandleUrl` give a direct path back to the uploader for licensing outreach, and `is_paid_content` flags videos that are commercially gated rather than free to reuse.

#### 📊 Market research and content intelligence

Run several related phrases in one job and export `foundViaKeyword`, `viewCount`, `commentCount` and `appliedFilters` to compare how a niche is covered across upload-date buckets or exact date windows — for example, pulling every video published in one launch month above a view floor to gauge coverage of a product announcement.

#### 🤖 AI agents and automated pipelines

`transcripts[].content` and `description` are ready to feed straight into an LLM for summarization or a RAG index, keyed on the stable `id` field. Because the actor runs through the standard Apify API, a scheduled pipeline can call it, wait for the run to finish, and pull filtered, transcript-attached rows with no manual step in between.

***

### 🔼 Input sample

Configure the run from the Actor's input form in Apify Console, or send the same fields as JSON through the Apify API. Nothing is individually required — every field has a working default — except that at least one of `keywords` or `urls` must be set, or the run fails immediately.

| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
| `keywords` | No | array of strings | One search phrase per line. Each phrase is run through YouTube search with your filter token attached. | `["drone footage"]` |
| `urls` | No | array of strings | Watch links, `youtu.be` links, `/shorts/`, `/live/`, `/embed/` links or bare 11-character video IDs. Bypasses every search filter and is always scraped. | `["https://youtu.be/kX8XKlNRVYY"]` |
| `max_results_per_keyword` | No | integer (1–100000) | Filtered videos to enrich per phrase. Default `20`. A filtered phrase realistically yields 130–480 results before YouTube runs out of pages, so a higher value simply stops early. | `20` |
| `videoDuration` | No | enum | `any`, `under_3`, `under_4`, `3_20`, `4_20`, `over_20`. Default `any`. `under_3`/`3_20`/`over_20` match YouTube's live filter menu; `under_4`/`4_20` are legacy bands still enforced server-side but light no filter chip. | `"over_20"` |
| `uploadDate` | No | enum | `any`, `hour`, `today`, `week`, `month`, `year`. Default `any`. Ignored by YouTube's ranker when an exact date range is also set. `hour` is enforced but lights no chip. | `"month"` |
| `resultType` | No | enum | `video`, `shorts`, `movie`. Default `video`. Channels and playlists are not offered — those filters return zero video rows. | `"shorts"` |
| `videoFeatures` | No | array of strings | `hd`, `4k`, `subtitles`, `creative_commons`, `live`, `360`, `3d`, `hdr`, `vr180`. Default `[]`. All selected features are required simultaneously. | `["4k"]` |
| `rankBy` | No | enum | `relevance`, `view_count`, `upload_date`, `rating`. Default `relevance`. Only `relevance` and `view_count` re-order YouTube's live results; `upload_date`/`rating` are sent correctly but ignored by YouTube's current ranker. | `"view_count"` |
| `publishedAfter` | No | date (absolute or relative) | `2024-03-01` or `3 months`. Sent as a real `after:` search operator; wins over `uploadDate` when both are set. | `"2024-01-01"` |
| `publishedBefore` | No | date (absolute or relative) | `2024-03-31` or `1 week`. Sent as a real `before:` search operator. | `"2024-12-31"` |
| `minViews` | No | integer (min 0) | Drop videos below this exact view count. `0` = off. | `10000` |
| `minLikes` | No | integer (min 0) | Drop videos below this exact like count. Videos with no published like count are kept, not dropped. `0` = off. | `0` |
| `minDurationSeconds` | No | integer (min 0) | Exact second-level floor, tighter than `videoDuration`. `0` = off. | `0` |
| `maxDurationSeconds` | No | integer (min 0) | Exact second-level ceiling. `0` = off. | `0` |
| `titleContains` | No | array of strings | Case-insensitive; kept if the title contains at least one term. Checked on the search card first, before a page fetch is spent. | `[]` |
| `titleExcludes` | No | array of strings | Case-insensitive; dropped if the title contains any term. Also checked on the search card first. | `["reaction", "shorts compilation"]` |
| `max_comments` | No | integer (0–5000) | Top-level comments to attach per video. Default `2`. `0` skips comments. | `5` |
| `include_transcripts` | No | boolean | Fetch caption text when the video exposes it. Default `true`. | `true` |
| `transcript_format` | No | enum | `text` or `timestamps`. Default `text`. | `"text"` |
| `include_english_auto` | No | boolean | Include YouTube's machine-generated English captions alongside creator-uploaded ones. Default `false`. | `false` |
| `include_non_english` | No | boolean | Include caption tracks in other languages. Default `false`. | `false` |
| `maxTranscriptLanguages` | No | integer (1–50) | Maximum caption tracks kept per video after duplicate removal. Default `5`. | `5` |
| `transcriptTimeoutSeconds` | No | integer (10–600) | Per-video ceiling for caption downloading. Default `120`. The video is still saved without transcripts if it's hit. | `120` |
| `maxSearchPages` | No | integer (1–100) | Hard stop on YouTube search paging per phrase. Default `30`. Each page yields roughly 7–20 filtered results. | `30` |
| `sort_order` | No | enum | `relevance`, `upload_date`, `view_count`, `rating`. Default `relevance`. Legacy compatibility field; `rankBy` takes precedence when set. | `"relevance"` |
| `proxyConfiguration` | No | object | Apify Proxy configuration. Default `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "US"}`. | see below |

```json
{
  "keywords": ["drone footage", "cinematic b roll"],
  "videoDuration": "over_20",
  "resultType": "video",
  "videoFeatures": ["4k"],
  "publishedAfter": "2024-01-01",
  "publishedBefore": "2024-12-31",
  "rankBy": "view_count",
  "minViews": 10000,
  "titleExcludes": ["reaction", "shorts compilation"],
  "max_results_per_keyword": 25,
  "max_comments": 5,
  "include_transcripts": true,
  "transcript_format": "text",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "US"
  }
}
```

**Common pitfall:** setting `rankBy` to `upload_date` or `rating` does not actually reorder results — both values are still sent to YouTube correctly, but YouTube's live Prioritise menu only honours `relevance` and `view_count` today, so the response order won't change. Use `uploadDate` or an exact `publishedAfter`/`publishedBefore` range to control recency instead of expecting the sort to do it. Separately, `under_4` and `4_20` are legacy bands that still filter server-side but never appear in the output's `appliedFilters` array, since YouTube lights no chip for them — don't read a missing chip as proof the filter didn't apply.

***

### 🔽 Output sample

Output is typed, normalized JSON with one consistent row per video. Export from the **Storage** tab as JSON, CSV, Excel, HTML table or XML. Every field below is pushed on every saved row — the dataset's default view (titled "Filtered videos") renders all 39 keys as table columns, so nothing here is hidden behind the default view.

Each pushed row triggers one `row_result` pay-per-event charge. A video is only pushed — and only charged — once it clears two gates: your post-download filters (`minViews`, `minLikes`, `minDurationSeconds`, `maxDurationSeconds`, `titleContains`/`titleExcludes`) must pass, and the extracted row must carry a title, a channel identity, and either a view count or a healthy page with a real upload date. Videos rejected by your filters or by that health check are never written to the dataset and never billed — their video IDs and reasons are recorded instead in the run's key-value store, under the `SUMMARY` record's `warnings` and `failures` lists, alongside `saved`, `filteredOut` and `skipped` counts for the run.

```json
{
  "id": "kX8XKlNRVYY",
  "url": "https://www.youtube.com/watch?v=kX8XKlNRVYY",
  "title": "8 Hours of Relaxing 4K Drone Footage",
  "description": "Filmed across three continents over two years...",
  "lengthInSeconds": 28800,
  "uploadDatetime": "2024-02-11T14:00:09-08:00",
  "publish_date_since": "1 year ago",
  "category": "Travel & Events",
  "tags": ["drone", "4k", "nature"],
  "thumbnail": "https://i.ytimg.com/vi/kX8XKlNRVYY/hqdefault.jpg",
  "thumbnails_urls": [
    "https://i.ytimg.com/vi/kX8XKlNRVYY/default.jpg",
    "https://i.ytimg.com/vi/kX8XKlNRVYY/hqdefault.jpg"
  ],
  "channelID": "UCxxxxxxxxxxxxxxxxxxxxxx",
  "channelURL": "https://www.youtube.com/channel/UCxxxxxxxxxxxxxxxxxxxxxx",
  "channelUsername": "@RelaxationFilm",
  "channelDisplayName": "Relaxation Film",
  "channelSubscribers": "1.2M",
  "author_avatar_url": "https://yt3.ggpht.com/abcXYZ=s176-c-k-c0x00ffffff-no-rj",
  "is_verified": false,
  "viewCount": 1842991,
  "likeCount": 21044,
  "commentCount": 1200,
  "is_live_now": false,
  "description_links": ["https://instagram.com/relaxationfilm"],
  "is_comments_off": false,
  "is_members_only": false,
  "is_paid_content": false,
  "transcripts": [
    { "language": "EN", "content": "welcome to eight hours of relaxing drone footage..." }
  ],
  "comments": [
    {
      "id": "UgzAbCdEfGhIjKlMnOp",
      "text": "This is exactly what I needed for background footage.",
      "author": "creativeStudioUser",
      "authorChannelId": "UCyyyyyyyyyyyyyyyyyyyyyy",
      "likeCount": 12,
      "publishedTime": "3 months ago",
      "replyCount": 1
    }
  ],
  "isShort": false,
  "searchResultBadges": ["4K"],
  "foundViaKeyword": "drone footage",
  "searchResultRank": 3,
  "searchResultPage": 1,
  "searchFilterToken": "EgYQARgCcAE=",
  "searchQuerySent": "drone footage after:2024-01-01 before:2024-12-31",
  "appliedFilters": ["Videos", "Over 20 minutes", "4K", "Relevance"],
  "channelHandleUrl": "https://www.youtube.com/@RelaxationFilm",
  "playabilityStatus": "OK",
  "scrapedAt": "2026-07-30T09:14:02Z"
}
```

***

### How do you filter and target specific videos?

Three controls narrow a run from a broad keyword to exactly the videos you want. **Search versus direct URL:** `keywords` triggers the full YouTube-search pipeline with every filter attached, while `urls` bypasses search entirely — use it when you already know the videos and just want enrichment. **Scope precision:** stack a `videoDuration` band with an exact `publishedAfter`/`publishedBefore` window for the tightest server-side narrowing YouTube allows — the exact date range always wins over the coarser `uploadDate` bucket when both are set — and add `resultType` to split Videos, Shorts or Movies, plus `videoFeatures` for quality signals like `4k` or `creative_commons`. **Quality thresholds:** `minViews`, `minLikes`, `minDurationSeconds`/`maxDurationSeconds` and `titleContains`/`titleExcludes` run against the finished record, so a video can pass every YouTube-side filter and still be dropped before it's saved or charged. **Volume controls:** `max_results_per_keyword` caps rows per phrase, and `maxSearchPages` is a hard stop on paging so a rare filter combination can't page forever.

```json
{ "keywords": ["documentary"], "videoDuration": "over_20", "publishedAfter": "2024-01-01", "publishedBefore": "2024-06-30", "rankBy": "view_count" }
```

*Long-form documentaries published in one exact half-year window, highest-viewed first.*

```json
{ "keywords": ["product launch"], "resultType": "shorts", "uploadDate": "week", "minViews": 5000, "titleExcludes": ["ad", "sponsored"] }
```

*Organic Shorts from the last week above a view floor, excluding obvious ad copy.*

```json
{ "keywords": ["drone footage"], "videoFeatures": ["creative_commons", "4k"], "minDurationSeconds": 60, "max_results_per_keyword": 50 }
```

*Reusable, high-resolution footage over a minute long, up to 50 results per phrase.*

***

### ▶️ Want to try other SimpleAPI scrapers?

| Scraper | What it extracts |
|---|---|
| [TikTok Discover Scraper by Account Name & Bio](../tiktok-discover-scraper-by-account-name-bio) | TikTok accounts and their videos, discovered by keyword or handle |
| [TikTok Hashtag Scraper: Hashtag Reach & Co-Tag Stats](../tiktok-hashtag-scraper-hashtag-reach-co-tag-stats) | Hashtag reach, view counts and co-occurring tag statistics |
| [TikTok Profile Videos API Scraper & Reposted Videos](../tiktok-profile-videos-api-scraper-reposted-videos) | A known TikTok account's own video and repost history |
| [TikTok Creative Center Top Ads Insights](../Tiktok%20Ads%20Scraper) | TikTok Creative Center ad performance and insights |
| [Instagram Profile Reels Scraper With Creator Profiles](../instagram-profile-reels-scraper-with-creator-profiles) | Instagram reels joined with the creator's own profile data |
| [Instagram Followers Scraper](../Instagram%20Followers%20Scraper) | Follower lists for a given Instagram account |

***

### How to extract YouTube data programmatically

This actor runs like any other Apify Actor: one API call starts the run, and results land in a standard Apify dataset you pull with the Apify API or an official client library — no separate signup, credential or webhook surface beyond your Apify API token.

#### 🐍 Python example

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("SimpleAPI/youtube-metadata-scraper-by-duration-date-video-type").call(
    run_input={
        "keywords": ["drone footage"],
        "videoDuration": "over_20",
        "videoFeatures": ["4k"],
        "minViews": 10000,
    }
)

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["title"], row["viewCount"], row["channelUsername"])
```

#### 📊 Export to spreadsheets or CRM

Export the dataset as CSV directly from the **Storage** tab, or connect it through an [Apify integration](https://docs.apify.com/integrations) to Google Sheets or your CRM. Map `title` and `url` to identity columns, `viewCount`/`likeCount`/`commentCount` to engagement fields, and `channelDisplayName`/`channelHandleUrl` to an outreach or attribution column.

***

### Is it legal to scrape YouTube?

Scraping publicly visible YouTube pages is generally lawful: this actor reads only what YouTube already serves to any logged-out visitor — search results and watch pages that require no login to view. That said, several fields identify individuals rather than the platform itself: `channelUsername`, `channelDisplayName` and `author_avatar_url` identify the uploader, and `comments[].author`/`comments[].authorChannelId` identify commenters. Where the data you collect is tied to an identifiable person, the personal-data regime applies — **GDPR** in the EU/UK and **CCPA/CPRA** in California govern how you may store and reuse it, and you need a lawful basis for retention at scale. Nothing behind a login, age-gate or members-only wall is accessed; those pages are detected and skipped rather than saved. Consult legal counsel for commercial applications involving bulk storage of personal data.

***

### ❓ Frequently asked questions

#### What happens if a video is private, deleted, or age/region-restricted?

Its watch page comes back with an empty title or a `playabilityStatus` other than `OK`. The actor retries on a fresh proxy exit, and if the page still won't resolve, the video is skipped — not saved, not charged — and its ID and reason are listed in the run's `SUMMARY` record in the key-value store.

#### Can I get comments along with the video metadata?

Yes. Set `max_comments` (0–5000, default `2`) to attach top-level comments to every matched video automatically — no separate run or field is required. Set it to `0` to skip comments and run faster.

#### How accurate are the view, like and subscriber numbers?

`viewCount` and `likeCount` are exact integers read straight from the video page. `channelSubscribers` is the abbreviated text YouTube itself publishes to logged-out visitors (`"1.2M"`), because YouTube does not expose an exact subscriber count without a login. `commentCount` is parsed from the same abbreviated `K`/`M`/`B` display text the page shows, so it carries the same rounding YouTube's own page display does above roughly a thousand.

#### How many videos can I get per run?

`max_results_per_keyword` accepts 1–100,000 per search phrase, but a filtered phrase realistically yields 130–480 results before YouTube's own search paging runs out — set higher, the run simply stops early. `maxSearchPages` (1–100, default 30) is a separate hard stop on paging per phrase, and each page yields roughly 7–20 filtered results. Videos supplied through `urls` are not subject to either limit.

#### How do the duration band and the exact duration filters interact?

`videoDuration` is YouTube's own server-side band (`under_3`, `3_20`, `over_20`, plus the legacy `under_4`/`4_20`), applied before any video is downloaded. `minDurationSeconds`/`maxDurationSeconds` are a second, exact second-level window applied after download, so you can combine a coarse band with a precise cutoff — for example `videoDuration: "over_20"` with `minDurationSeconds: 1800` to keep only videos both server-flagged as long-form and confirmed to be at least 30 minutes.

#### Does YouTube Search Scraper work with Claude, ChatGPT, and AI agent frameworks?

There is no dedicated MCP server for this actor. Any agent framework that can call the Apify API — including custom tool wrappers around `apify-client` — can start a run, poll for completion, and read the resulting dataset as a standard HTTP call.

#### How does this compare to other YouTube scrapers?

As observed on their public Apify listings on 2026-07-30: [apidojo/youtube-playlist-scraper](https://apify.com/apidojo/youtube-playlist-scraper) is built around playlist and channel-series extraction with pay-per-event pricing, not filtered keyword search. [beyondops/youtube-metadata-scraper-pro-v2](https://apify.com/beyondops/youtube-metadata-scraper-pro-v2) enriches a list of video URLs you supply and recommends pasting browser cookies for age-restricted or blocked content, but documents no server-side search filtering by duration, date or type. [trisecode/yt-transcript](https://apify.com/trisecode/yt-transcript) takes one `video_url` at a time for metadata and transcript, with no keyword search or filter parameters documented at all. This actor's search path is the only one of the three that filters by duration band, upload date, exact date range and Videos/Shorts/Movies type before a single video is downloaded.

#### Can I use YouTube Search Scraper without a YouTube API key or developer account?

Yes. No YouTube Data API key, Google Cloud project, YouTube account, cookie or login is required anywhere in this actor — you only need an Apify account to run it.

***

### Conclusion

YouTube Search Scraper turns a keyword and a set of real YouTube filters into a structured dataset of fully enriched videos — exact engagement numbers, real channel handles, comments and transcripts, with the literal search filters and result rank recorded on every row. It's built for researchers, footage and licensing teams, market-intelligence analysts and AI pipelines that need YouTube's own search filtering without a Data API key or its daily quota. Open the actor in Apify Console, drop your first search phrase into **Search phrases**, and click **Start** to see your first filtered set of videos.

# Actor input Schema

## `keywords` (type: `array`):

One phrase per line. Each phrase is run through YouTube search with your filter token attached. Example: `drone footage` + duration `Over 20 minutes` + feature `4K`.

## `max_results_per_keyword` (type: `integer`):

How many filtered videos to enrich per phrase. Deep paging goes through YouTube's own continuation endpoint; a filtered phrase realistically yields 130–480 results before YouTube runs out of pages, so anything higher simply stops early. Default 20.

## `urls` (type: `array`):

Optional. Watch links, `youtu.be` links, `/shorts/`, `/live/`, `/embed/` links or bare 11-character video IDs. Videos listed here bypass every search filter and are always scraped.

## `videoDuration` (type: `string`):

YouTube's live filter bar only offers Under 3 / 3–20 / Over 20; those three were exact (0 out-of-band results in 84 deep-paged rows). The two legacy 4-minute bands still work on the wire but are approximate at the boundary — a live run returned one 239-second video inside the 4-to-20 band — and YouTube lights no chip for them, so they never appear in the `appliedFilters` column. Use the exact second-level window below when the boundary matters.

## `uploadDate` (type: `string`):

Coarse recency bucket. If you also set an exact date range below, the exact range wins and this bucket is ignored by YouTube's ranker. `Last hour` is enforced (measured: 15/18 results minutes old vs 8/20 hours old with no filter) but lights no chip.

## `resultType` (type: `string`):

Only these three types return actual videos. Channels and playlists are deliberately not offered: those filters return channel and playlist cards, so a run would finish with zero video rows. Shorts results arrive in two different card layouts depending on the phrase — both are parsed.

## `videoFeatures` (type: `array`):

Pick any combination — YouTube requires all of them at once. Note `Movies` plus a feature flag is usually over-constrained and returns very few rows.

## `rankBy` (type: `string`):

YouTube's live Prioritise menu now contains only Relevance and View count. Upload date and Rating are still accepted and sent correctly, but YouTube ignores them — use the upload-date bucket or the exact date range instead. Leave empty to fall back to the legacy `sort_order` field.

## `publishedAfter` (type: `string`):

Absolute (`2024-03-01`) or relative (`3 months`, `2 weeks`). Videos published before this date are excluded by YouTube itself.

## `publishedBefore` (type: `string`):

Absolute (`2024-03-31`) or relative (`1 week`). Videos published after this date are excluded by YouTube itself.

## `minViews` (type: `integer`):

Drop videos below this exact view count. 0 = off.

## `minLikes` (type: `integer`):

Drop videos below this exact like count. Videos whose like count is not published are kept, not dropped. 0 = off.

## `minDurationSeconds` (type: `integer`):

Exact second-level floor, tighter than the coarse length band above. 0 = off.

## `maxDurationSeconds` (type: `integer`):

Exact second-level ceiling. 0 = off.

## `titleContains` (type: `array`):

Case-insensitive. A video is kept if its title contains at least one of these. Checked on the search card first, so non-matching videos are dropped before they cost a page fetch.

## `titleExcludes` (type: `array`):

Case-insensitive. A video is dropped if its title contains any of these. Also checked on the search card first.

## `max_comments` (type: `integer`):

Top-level comments to attach to each matched video. Set 0 to skip comments entirely and run faster.

## `include_transcripts` (type: `boolean`):

Fetch caption text when the video exposes it. Coverage measured at 86% of videos on a residential exit; near zero on datacenter, which is why the network defaults to residential.

## `transcript_format` (type: `string`):

Plain text is compact; timed segments keep the per-line timing.

## `include_english_auto` (type: `boolean`):

Include YouTube's machine captions in English alongside creator-uploaded ones. Identical duplicate tracks are removed automatically.

## `include_non_english` (type: `boolean`):

Some videos expose 38+ language tracks, each a separate download. Use the language cap below to keep runs bounded.

## `maxTranscriptLanguages` (type: `integer`):

Maximum caption tracks stored per video after duplicate removal. Protects row size — one measured transcript was over 500,000 characters.

## `transcriptTimeoutSeconds` (type: `integer`):

Per-video ceiling for caption downloading. When it is hit the video is still saved, just without transcripts.

## `maxSearchPages` (type: `integer`):

Hard stop on YouTube search paging, so a rare filter cannot page forever. Each page yields roughly 7–20 filtered results.

## `sort_order` (type: `string`):

Kept so input built for the standard YouTube Metadata Scraper still runs unchanged. `Prioritise results by` above takes precedence when you set it.

## `proxyConfiguration` (type: `object`):

Defaults to Apify Residential proxy in the United States, which is what YouTube requires here: 83% of datacenter watch-page fetches come back as HTTP 200 with an empty title and a login-required playability status, and caption downloads fail there entirely. Your selection is honoured; if it starts returning blocked pages the run escalates to residential and says so in the log.

## Actor input object example

```json
{
  "keywords": [
    "drone footage"
  ],
  "max_results_per_keyword": 20,
  "videoDuration": "any",
  "uploadDate": "any",
  "resultType": "video",
  "videoFeatures": [],
  "rankBy": "relevance",
  "minViews": 0,
  "minLikes": 0,
  "minDurationSeconds": 0,
  "maxDurationSeconds": 0,
  "max_comments": 2,
  "include_transcripts": true,
  "transcript_format": "text",
  "include_english_auto": false,
  "include_non_english": false,
  "maxTranscriptLanguages": 5,
  "transcriptTimeoutSeconds": 120,
  "maxSearchPages": 30,
  "sort_order": "relevance",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All scraped items in the Actor's default dataset.

## `summary` (type: `string`):

A single JSON record with run totals: rows saved, filtered out and skipped, the resolved YouTube sp= filter token, the resolved date range, non-fatal warnings, and per-video failure reasons.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "drone footage"
    ],
    "max_results_per_keyword": 20,
    "resultType": "video",
    "max_comments": 2,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("simpleapi/youtube-metadata-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "keywords": ["drone footage"],
    "max_results_per_keyword": 20,
    "resultType": "video",
    "max_comments": 2,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("simpleapi/youtube-metadata-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "drone footage"
  ],
  "max_results_per_keyword": 20,
  "resultType": "video",
  "max_comments": 2,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call simpleapi/youtube-metadata-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,simpleapi/youtube-metadata-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/BWbXwgx4I5fTFiwtK/builds/b5fvbkG07wvpGYccx/openapi.json
