# YouTube Search Results & Rank Tracker Scraper (`heyitsgautham/youtube-search-scraper`) Actor

Give it keywords and get YouTube search results with rank, title, channel, views and duration. Rank tracker mode compares keyword rankings with the last run: rank change, new and dropped results, target videos or channels flagged, several countries per run. $0.24 per 1,000 results, failures free.

- **URL**: https://apify.com/heyitsgautham/youtube-search-scraper.md
- **Developed by:** [Gautham Krishna](https://apify.com/heyitsgautham) (community)
- **Categories:** Videos, Social media, SEO tools
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.18 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## YouTube Search Results & Rank Tracker Scraper

Get YouTube search results as clean rows with rank, title, channel and views, for SEO teams tracking keyword rankings and marketers looking for creators.

Use cases, each with a ready-to-run example:

- [Track where a channel ranks for a keyword](https://apify.com/heyitsgautham/youtube-search-scraper/examples/youtube-keyword-rank-tracker)
- [Compare search results in the US, the UK and India](https://apify.com/heyitsgautham/youtube-search-scraper/examples/youtube-search-results-by-country)
- [Find the most viewed videos for a keyword](https://apify.com/heyitsgautham/youtube-search-scraper/examples/most-viewed-youtube-videos-for-keyword)
- [Search YouTube Shorts for a keyword](https://apify.com/heyitsgautham/youtube-search-scraper/examples/youtube-shorts-search)
- [Find small creators in a niche by subscriber count](https://apify.com/heyitsgautham/youtube-search-scraper/examples/find-small-youtube-creators)
- [Get views, likes and tags for a list of video URLs](https://apify.com/heyitsgautham/youtube-search-scraper/examples/youtube-video-details-from-urls)

Each result also has the duration, upload age, thumbnail and snippet.
Add video URLs to get full details for specific videos too: description, tags, likes, category and upload date.
Rank tracker mode compares each result with the last run, flags your own or competitors' videos and channels, and covers several countries in one run.
Channel discovery finds YouTube channels on a topic within a subscriber range, such as small creators with 1,000 to 100,000 subscribers for outreach.

You pay **$0.24 per 1,000 results** ($0.00024 each), and less on Apify Store discount tiers (Bronze, Silver, Gold), down to $0.18 per 1,000 on Gold.
Failed queries, unavailable videos, invalid input, `dropped` rows and channels left out by a subscriber range are **never charged**.
In the final test run, 56 queries in 15 languages all returned results, and all 50 real videos in the lookup returned full details.

### What you get per result

| Field | What it holds |
|---|---|
| `query`, `rank` | The search it came from and its position, starting at 1 |
| `type` | `video`, `short`, `channel`, `playlist` or `movie` |
| `id`, `url` | Video, channel or playlist ID and its link |
| `title`, `thumbnail` | As shown in the results |
| `channelName`, `channelId`, `channelUrl`, `channelVerified` | The uploader. Joint uploads also list every channel in `collaborators` |
| `viewCount`, `viewCountText` | Views as a number and as YouTube shows them |
| `duration`, `durationSeconds` | Length, such as `1:00:06` and `3606` |
| `publishedTimeText` | Upload age as shown, such as `6 years ago` |
| `descriptionSnippet` | The description excerpt shown under the result |
| `badges` | Labels such as `4K`, `CC`, `New` or `LIVE` |
| `isLive`, `watchingCount` | Live streams show viewers watching now instead of views |
| `subscriberCountText`, `channelHandle` | On channel results |
| `subscriberCount` | On channel results, the subscriber count as a number, such as `90600` for `90.6K subscribers`. It is as rounded as YouTube shows it, and `null` when the channel hides its count. It is always the last field of a channel row |
| `videoCountText` | On playlist and channel results |
| `status` | `ok`, `no_results`, `unavailable`, `invalid_input`, `error`, or `dropped` (rank tracker only) |

Rows from the **video lookup** add:

| Field | What it holds |
|---|---|
| `description` | The full description |
| `keywords` | The uploader's tags |
| `likeCount` | Likes, when the uploader shows them |
| `category` | Such as `Education` or `Music` |
| `publishDate`, `uploadDate` | Exact dates with time zone |
| `isLive`, `isLiveContent`, `liveStartTime`, `liveEndTime` | Live status and stream times |
| `isShort`, `isFamilySafe`, `isUnlisted` | Flags from YouTube |

Rows from the **rank tracker** options add:

| Field | When | What it holds |
|---|---|---|
| `country` | `countries` is set | The country this result was searched from |
| `isTarget` | `targets` is set | `true` when the result is one of your target videos, or comes from one of your target channels |
| `previousRank` | `compareWithLastRun` is on | Its rank in the last run, or `null` if it was not in it |
| `rankChange` | `compareWithLastRun` is on | `previousRank` minus `rank`, so `3` means it moved up three places |
| `isNew` | `compareWithLastRun` is on | `true` when it was not in the last run's results. All three fields are `null` on the first run |

### Input

```json
{
  "queries": ["python tutorial for beginners", "receta de paella valenciana"],
  "maxResultsPerQuery": 40,
  "type": "video",
  "uploadDate": "month",
  "duration": "long",
  "sortBy": "viewCount",
  "videos": ["https://www.youtube.com/watch?v=jNQXAC9IVRw", "dQw4w9WgXcQ"]
}
```

| Option | Values |
|---|---|
| `type` | `all`, `video`, `shorts`, `channel`, `playlist`, `movie` |
| `uploadDate` | `any`, `hour`, `today`, `week`, `month`, `year` |
| `duration` | `any`, `short` (under 4 min), `medium` (4 to 20 min), `long` (over 20 min) |
| `sortBy` | `relevance`, `viewCount` |
| `features` | `live`, `4k`, `hd`, `subtitles`, `creativeCommons`, `360`, `vr180`, `hdr` |
| `language`, `country` | Interface language such as `en` and two-letter country such as `US`. Results are localised for that country |
| `includeShortsShelves` | Also return the Shorts that YouTube shows in shelves between results |
| `countries` | Several two-letter countries, such as `["US", "IN", "DE"]`. Every query runs once per country |
| `compareWithLastRun` | Rank tracker mode, see below |
| `targets` | Videos (URL or ID) and channels (`@handle`, channel URL or `UC...` ID) to flag with `isTarget` |
| `minSubscribers`, `maxSubscribers` | Keep only channels in this subscriber range, see below |

You can send only `queries`, only `videos`, or both.
The video lookup accepts watch, youtu.be, Shorts, embed and live links, plus bare 11-character IDs.

### Output example

These are real rows from a test run.
A search result:

```json
{
  "query": "python tutorial for beginners",
  "rank": 1,
  "type": "video",
  "id": "kqtD5dpn9C8",
  "url": "https://www.youtube.com/watch?v=kqtD5dpn9C8",
  "title": "Python for Beginners - Learn Coding with Python in 1 Hour",
  "channelName": "Programming with Mosh",
  "channelId": "UCWv7vMbMWH4-V0ZXdmDpPBA",
  "channelUrl": "https://www.youtube.com/@programmingwithmosh",
  "viewCount": 25573529,
  "viewCountText": "25,573,529 views",
  "duration": "1:00:06",
  "durationSeconds": 3606,
  "publishedTimeText": "6 years ago",
  "thumbnail": "https://i.ytimg.com/vi/kqtD5dpn9C8/hq720.jpg?sqp=-oaymwEXCNAFEJQDSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAlOSbnBnoUI-N9v2_o-DYUecBN7A",
  "descriptionSnippet": "Learn Python basics in just 1 hour! Perfect for beginners interested in AI and coding. ⚡ Plus, get 6 months of PyCharm FREE with ...",
  "badges": [],
  "isLive": false,
  "channelVerified": true,
  "collaborators": [],
  "watchingCount": null,
  "status": "ok"
}
```

A video lookup, with the search-only fields that are null on these rows (`query`, `rank`, `viewCountText` and so on) left out:

```json
{
  "type": "video",
  "id": "jNQXAC9IVRw",
  "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "title": "Me at the zoo",
  "channelName": "jawed",
  "channelId": "UC4QobU6STFB0P71PMvOGN5A",
  "channelUrl": "https://www.youtube.com/@jawed",
  "viewCount": 441938487,
  "duration": "0:19",
  "durationSeconds": 19,
  "thumbnail": "https://i.ytimg.com/vi/jNQXAC9IVRw/hqdefault.jpg?sqp=-oaymwEmCOADEOgC8quKqQMa8AEB-AG-AoAC8AGKAgwIABABGFUgWShlMA8=&rs=AOn4CLA9eLBatYv9WbkD4BbZ2Im-biSPTw",
  "isLive": false,
  "input": "https://www.youtube.com/watch?v=jNQXAC9IVRw&t=10",
  "status": "ok",
  "description": "Microplastics are accumulating in human brains at an alarming rate\nhttps://www.youtube.com/watch?v=0PT5c1z3LL8\n\n“Nanoplastics and Human Health” with Matthew J Campen, PhD, MSPH\nhttps://www.youtube.com/watch?v=RRBN_4L09Mg\n\n00:00 Intro\n00:05 The cool thing\n00:17 End",
  "keywords": ["me at the zoo", "jawed karim", "first youtube video"],
  "likeCount": 20034803,
  "category": "Film & Animation",
  "publishDate": "2005-04-23T20:31:52-07:00",
  "uploadDate": "2005-04-23T20:31:52-07:00",
  "isLiveContent": false,
  "liveStartTime": null,
  "liveEndTime": null,
  "isShort": false,
  "isFamilySafe": true,
  "isUnlisted": false
}
```

A video that does not exist comes back as `"status": "unavailable", "message": "Video unavailable"` and costs nothing.

### Find channels by subscriber count

Set `minSubscribers`, `maxSubscribers` or both to get channels on a topic within a subscriber range.

```json
{
  "queries": ["sourdough baking", "watercolor painting", "home espresso"],
  "type": "channel",
  "maxResultsPerQuery": 50,
  "minSubscribers": 1000,
  "maxSubscribers": 100000
}
```

A kept channel from the test run, with the empty fields left out:

```json
{
  "query": "sourdough baking",
  "rank": 4,
  "type": "channel",
  "id": "UCym_8JHA4htlFLIHGpNZGrQ",
  "url": "https://www.youtube.com/@FullProofBaking",
  "title": "Full Proof Baking",
  "channelName": "Full Proof Baking",
  "channelId": "UCym_8JHA4htlFLIHGpNZGrQ",
  "channelUrl": "https://www.youtube.com/@FullProofBaking",
  "thumbnail": "https://yt3.googleusercontent.com/ytc/AIdro_ndWmzk7nuUIL6cUYxx64d_L0Vjq4foburvGQ33-j5GHw=s176-c-k-c0x00ffffff-no-rj-mo",
  "descriptionSnippet": "Sourdough-obsessed home baker from Chicago.",
  "channelVerified": false,
  "channelHandle": "@FullProofBaking",
  "subscriberCountText": "90.6K subscribers",
  "status": "ok",
  "subscriberCount": 90600
}
```

A range always searches channels.
With `type` set to `all`, the search switches to channels, since mixed results hold too few channels to fill a list.
With `type` set to `video`, `shorts`, `playlist` or `movie`, the run returns one free row that explains the mismatch.

`rank` stays the channel's position in YouTube's channel results, so ranks skip the channels left out.
In the search above, ranks 2 and 3 were outside the range.

`maxResultsPerQuery` counts the channels kept.
The scraper reads more pages to fill it while at least about a third of the channels read so far are in range, then stops, so a narrow range can return fewer.
Channels outside the range, and channels that hide their count, are left out and cost nothing.
The status message says how many were left out.

The count comes from the text YouTube shows in your `language`, such as `1.2M`, `51,5 K`, `124.000`, `22.2 लाख` or `12.4万`.
A channel near a limit can fall either side of it as its rounded count changes.
The dataset's **Channels** view shows rank, channel, handle, subscribers and link side by side.

The output has public channel details only, with no emails or other contact data.

### Rank tracker mode

Turn on `compareWithLastRun` and schedule the same input daily or weekly.
Each run compares every query in every country with the last complete run of it in your account.

```json
{
  "queries": ["python tutorial for beginners", "receta de paella valenciana"],
  "maxResultsPerQuery": 20,
  "countries": ["US", "IN", "DE"],
  "compareWithLastRun": true,
  "targets": ["@programmingwithmosh", "UC8butISFwT-Wl7EV0hUK0BQ", "https://youtu.be/kqtD5dpn9C8"]
}
```

A result from the second run of a test, with some search fields left out:

```json
{
  "query": "python tutorial for beginners",
  "rank": 2,
  "type": "video",
  "id": "kqtD5dpn9C8",
  "url": "https://www.youtube.com/watch?v=kqtD5dpn9C8",
  "title": "Python for Beginners - Learn Coding with Python in 1 Hour",
  "channelName": "Programming with Mosh",
  "channelId": "UCWv7vMbMWH4-V0ZXdmDpPBA",
  "viewCount": 25639490,
  "isTarget": true,
  "previousRank": 1,
  "rankChange": -1,
  "isNew": false,
  "country": "US",
  "status": "ok"
}
```

A result that was in the last run and is gone now comes back as a free `dropped` row, with its last rank and the details it had then.
This one is from the same run, with the empty fields left out:

```json
{
  "query": "python tutorial for beginners",
  "rank": null,
  "type": "video",
  "id": "qwAFL1597eM",
  "url": "https://www.youtube.com/watch?v=qwAFL1597eM",
  "title": "Python Tutorial for Beginners (with mini-projects)",
  "channelName": "freeCodeCamp.org",
  "channelId": "UC8butISFwT-Wl7EV0hUK0BQ",
  "previousRank": 18,
  "rankChange": null,
  "isNew": false,
  "status": "dropped",
  "message": "Ranked 18 in the last run on 2026-10-16, missing from this run's 20 results",
  "isTarget": true,
  "country": "US"
}
```

Each query, country, language, filter set and max results value has its own history.
Change any of them and the next run starts a new history, because a deeper or differently filtered list is not comparable.
A query that failed, returned nothing or stopped at your maximum charge keeps the previous history, so the next run still compares with the last complete run.
The history lives in a key-value store named `youtube-search-scraper-monitor` in your own Apify account.
Delete it to start over.

The dataset's **Rank tracker** view shows country, rank, last run rank, change, new and target side by side.

With a subscriber range on, the range is part of the history too.
A channel from the last run counts as `dropped` only when this run read past its old rank and did not find it.
A channel this run found outside the range, or one past where this run stopped reading, is not a dropped row.

Targets given as `@handle`, `/c/` or `/user/` links are looked up once per run to find the channel ID, so they also match results whose channel link has no handle.
A target YouTube does not know comes back as a free `invalid_input` row.

### Run it on a schedule

A daily or weekly run of the rank tracker shows how your videos and channels move for the searches you care about.
We tracked 60 channel searches on 17 Oct and ran them again on 20 Oct.
Of the 1,423 channels returned, 338 were new to their search, 973 had moved and 112 held their rank, and 280 free `dropped` rows named the channels that fell out.

1. Set your input with `compareWithLastRun: true` and click **Save as a new task**.
2. On the task page, click **Actions**, then **Schedule**, and pick how often it runs.
   [Apify's schedules guide](https://docs.apify.com/actors/running/schedules) covers cron times and time zones.
3. On the task's **Integrations** tab, choose where each run's results go.

| Integration | What it does |
|---|---|
| [Slack](https://docs.apify.com/integrations/slack) | Posts a message to your channel when a run finishes |
| [Google Drive](https://docs.apify.com/integrations/drive) | Saves each run's results as a file in your Drive |
| [Webhook](https://docs.apify.com/integrations/webhooks) | Sends the finished run, with its dataset ID, to your own URL |
| [Make](https://docs.apify.com/integrations/make), [Zapier](https://docs.apify.com/integrations/zapier), [n8n](https://docs.apify.com/integrations/n8n) | Adds each new row to a Google Sheet, a CRM or a database |

Each row has a stable `id`, the video or channel ID, so a sheet can chart one result's rank across runs.

### How well it works

In the test run, 56 search queries in 15 languages (English, Spanish, Hindi, Tamil, Japanese, Korean, Arabic, Russian, Portuguese, German, French, Chinese, Turkish, Indonesian and Vietnamese) all returned results, 40 per query for most.
All 50 real videos in the lookup returned full details.
The Shorts, channel, playlist, movie, upload date, duration, sort and feature filters were each checked in separate runs.

In the rank tracker test, 56 queries in 3 countries (US, IN and DE) ran twice, 5 minutes apart.
All 168 query and country pairs returned results both times.
A script checked every `previousRank`, `rankChange`, `isNew` and `dropped` row of the second run against the first run's ranks, and found no mismatch.
Every result from the 4 target channels and the target video was flagged, and no other result was.

In the channel discovery test, 60 channel searches in English with a 1,000 to 100,000 subscriber range all returned channels in range, 2,096 in all, while 3,022 channels outside it were left out for free.
20 of the kept channels were checked against their channel pages, and all 20 counts matched.
Runs in Japanese, Hindi, Spanish and German returned channels in range for all 20 of their searches.
The count reader was also checked on the same searches in 83 interface languages, and all 2,067 counts it could compare matched the English count.
In a rank tracker test with the range on, the same 60 searches ran twice, and a script found no mismatch in the second run's `previousRank`, `rankChange`, `isNew` and 253 `dropped` rows.

### Use it from AI agents

The scraper works as a tool through the [Apify MCP server](https://mcp.apify.com) in Claude, Cursor, VS Code and other MCP clients.
An agent can search YouTube for a topic and get back ranked rows it can filter, summarise or pass to a transcript tool.

You can also call it through the [Apify API](https://docs.apify.com/api/v2) or the Python and JavaScript clients, and schedule it or connect it to Make, Zapier and n8n.

### Pricing

Prices are per 1,000 results.

| Event | No discount | Bronze | Silver | Gold |
|---|---|---|---|---|
| Result returned (a search row or a looked-up video) | $0.24 | $0.22 | $0.20 | $0.18 |
| Query with no results, unavailable video, invalid input, failed request, `dropped` row or channel outside the subscriber range | Free | Free | Free | Free |

Bronze, Silver and Gold are Apify Store discount tiers, and Apify's Pricing tab shows them as Starter, Scale and Business.
Apify decides which tier an account is on.
Set a maximum cost per run in the run options.
The scraper stops once that limit is reached.

### More from heyitsgautham

Other actors from the same account.

- [YouTube Transcript & Subtitles Scraper](https://apify.com/heyitsgautham/youtube-transcript-scraper) for specific videos, or when you want every video on a channel or playlist with channel stats and up to 20,000 videos per source.
- [YouTube Video & Shorts Comments Scraper with Replies](https://apify.com/heyitsgautham/youtube-comments-scraper) for the comments and replies on a video or Short, for sentiment analysis and audience research.
- [YouTube Channel & Playlist Transcripts](https://apify.com/heyitsgautham/youtube-channel-transcripts) for every video on a channel or playlist, with channel stats and up to 20,000 videos per source.
- [YouTube & TikTok Transcripts MCP Server for AI Agents](https://apify.com/heyitsgautham/video-transcripts-mcp-server) for giving an AI agent YouTube and TikTok transcripts, YouTube search and comments as MCP tools, with nothing to host.
- [Google Trends Scraper: Related Queries & Trending Searches](https://apify.com/heyitsgautham/google-trends-scraper) for Google Trends interest over time, related queries and Trending Now.

### FAQ

**Why did a query return fewer results than I asked for?**
YouTube decides how deep a search goes.
With result type All, YouTube often runs out of regular results after 30 to 60 and fills the rest of the feed with Shorts shelves.
Set `type` to `video` for longer lists, or turn on `includeShortsShelves` to get those Shorts as rows.
A narrow filter such as `uploadDate: today` on a niche query can return only a handful of results, because that is all YouTube shows.

**Why did a subscriber range return fewer channels than I asked for?**
YouTube's channel results for a topic mix sizes, and big channels tend to come first.
The scraper keeps reading while at least about a third of the channels read are in your range, so a narrow range such as 1,000 to 10,000, or a high floor such as 1,000,000, can end with fewer channels.
Try a wider range, or several related queries.

**Can I sort by upload date?**
YouTube removed that sort option from search in 2025.
Use the `uploadDate` filter instead.

**Why do Shorts rows have no `publishedTimeText`?**
YouTube shows Shorts in shelves without a channel or date.
The scraper fills channel, exact views, duration and `publishDate` with one extra small request per Short, so you get the exact date instead of text like "2 days ago".

**Why do ranks move between two runs a few minutes apart?**
YouTube does not show every logged-out visitor the same order, and each run reaches YouTube from different residential IPs in the country.
In the rank tracker test, the second run, 5 minutes after the first, had 1,673 top-10 results.
759 of them were at most one place from their last rank, 406 sat at the same rank, and 260 were new.
Read single moves of one or two places as noise, and look at trends over several runs.

**Does it use the official YouTube Data API?**
No.
It reads the same public data the YouTube website shows to a logged-out visitor, so you need no API key and have no daily quota.

**Are view counts exact?**
Yes for videos.
Live streams have `viewCount: null` and the number of people watching in `watchingCount`.

### Limitations

- Results depend on the `country` and `language` you pick, and on YouTube's own ranking, which changes over time.
- Mixes (auto-generated playlists) have no single channel, so their channel fields are empty.
- Like counts are missing when the uploader hides them.
- Subscriber counts are the rounded figures YouTube shows, such as 90.6K. YouTube does not show exact counts to visitors.
- YouTube can change its site at any time. If something breaks, open an issue on the Issues tab.

# Actor input Schema

## `queries` (type: `array`):

What you would type into the YouTube search box. One row per result, ranked in the order YouTube shows them.

## `maxResultsPerQuery` (type: `integer`):

Stops after this many results for each query. With result type All, YouTube often runs out of regular results after 30 to 60 and fills the feed with Shorts. Pick Videos for longer lists.

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

Limit results to one type. All returns videos, channels and playlists as YouTube mixes them. Shorts rows get channel, exact views, duration and upload date from one extra small request each.

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

Only results uploaded within this window.

## `duration` (type: `string`):

Under 4 minutes, 4 to 20 minutes, or over 20 minutes.

## `sortBy` (type: `string`):

Relevance or view count. YouTube removed sorting by upload date in 2025. Use the upload date filter instead.

## `features` (type: `array`):

Only results that have all of these features.

## `includeShortsShelves` (type: `boolean`):

Also return the Shorts that YouTube shows in horizontal shelves between results. Each one gets channel, exact views, duration and upload date from one extra small request.

## `minSubscribers` (type: `integer`):

Optional. Keep only channels with at least this many subscribers, such as 10000. Uses the count YouTube shows, so 1.2M counts as 1,200,000. Turns the search into a channel search, and channels that hide their count are left out. Left-out channels are free.

## `maxSubscribers` (type: `integer`):

Optional. Keep only channels with at most this many subscribers, such as 100000 to find small creators. Works alone or with Min subscribers.

## `compareWithLastRun` (type: `boolean`):

Each result gets previousRank, rankChange (positive means it moved up) and isNew, against the last complete run of the same query, country, language, filters and max results in your account. Results from that run that are gone come back as free rows with status dropped. The first run has nothing to compare with, so those fields are null. History is kept in a key-value store named youtube-search-scraper-monitor in your account.

## `targets` (type: `array`):

Optional. Your own or competitors' videos (URL or 11-character ID) and channels (@handle, channel URL or UC... ID). Results from them get isTarget: true, so you can filter to where they rank.

## `videos` (type: `array`):

Optional. Get full details for specific videos: description, keywords, like count, category, upload date, live and Shorts flags. Accepts watch, youtu.be, Shorts, embed and live links, or 11-character IDs.

## `language` (type: `string`):

YouTube interface language code such as en, es, de or hi. It changes text like "3 days ago" and "views".

## `country` (type: `string`):

Two-letter country code such as US, GB, IN or BR. Results are localised for this country, and the proxy uses IPs from it.

## `countries` (type: `array`):

Optional. Run every query once per country, such as US, GB and IN, each through residential IPs from that country. Every result row then has a country field. Replaces Country when set.

## `proxyInCountry` (type: `boolean`):

Use residential IPs from the chosen country, so results match what local viewers see. Turn off to use IPs from any country.

## `maxConcurrency` (type: `integer`):

How many queries or video lookups run in parallel.

## `useProxy` (type: `boolean`):

Leave on. YouTube blocks cloud IPs.

## Actor input object example

```json
{
  "queries": [
    "lofi hip hop",
    "how to make sourdough bread"
  ],
  "maxResultsPerQuery": 20,
  "type": "all",
  "uploadDate": "any",
  "duration": "any",
  "sortBy": "relevance",
  "includeShortsShelves": false,
  "compareWithLastRun": false,
  "language": "en",
  "country": "US",
  "proxyInCountry": true,
  "maxConcurrency": 10,
  "useProxy": true
}
```

# Actor output Schema

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

One item per search result or looked-up video, with type, rank, title, channel, views, duration and thumbnail. Video lookups add the full description, keywords, likes and category. Only items with status ok are charged.

# 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 = {
    "queries": [
        "lofi hip hop",
        "how to make sourdough bread"
    ],
    "maxResultsPerQuery": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("heyitsgautham/youtube-search-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 = {
    "queries": [
        "lofi hip hop",
        "how to make sourdough bread",
    ],
    "maxResultsPerQuery": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("heyitsgautham/youtube-search-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 '{
  "queries": [
    "lofi hip hop",
    "how to make sourdough bread"
  ],
  "maxResultsPerQuery": 20
}' |
apify call heyitsgautham/youtube-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,heyitsgautham/youtube-search-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/skK1AQHipdsrlNG9x/builds/hcDQ6bObJnlTUYmEk/openapi.json
