# YouTube Search Scraper - Keyword Rankings (`tubelens/youtube-search-scraper`) Actor

Give it keywords, get YouTube's ranked results: position, title, channel, views and publish date for every result. Built for rank tracking and competitor research. No transcripts or comment text: the Video Scraper does those.

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

## Pricing

from $2.00 / 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.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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 - track keyword rankings and search results

Give it keywords, get YouTube's ranked results. Each row is one result with its position, so you always know what rank a video, channel or playlist holds for that search right now.

**$0.005 per run start plus $0.002 per result, that's $2 per 1,000 results.**

Takes a list of keywords, not just one, so you can track a whole set in a single run.

### Why this scraper?

- 🔑 **No quota wall.** YouTube's own Data API charges 100 units per search against a 10,000-unit daily default, about 100 searches a day, and requests to raise that limit for competitive analytics are frequently turned down.
- 📋 **Bulk keywords, not one at a time.** Paste a list and every row carries the keyword that produced it. The dedicated search actor in this niche takes a single query string, so bulk sets are impossible there.
- 🥇 **Real rank position.** Every result carries its 1-based position, counting only what a person would actually scroll past. It works in all five result types: videos rank against videos, channels against channels, playlists against playlists, Shorts against Shorts. Shelves and other page furniture never take a number.
- 🕳️ **Deep paging, not just the first screen.** Around 300 to 500 unique results for a specific keyword, well past where cheaper scrapers stop. Very broad one-word terms exhaust much sooner.
- 🎯 **Filters that are honest about what works.** Sort is Relevance or Popularity because those are the only two that actually reorder anything on YouTube's own page; upload window, duration and content filters all measurably change what comes back.
- 🌍 **Language genuinely changes the results.** Set it to match your audience and get a different ranked list, not just relabeled text. View counts and publish dates come back as real numbers and real dates in 14 languages, Indian lakh grouping and Arabic word-counts included, not just in English.
- 🚫 **No proxy setup, no browser to babysit.** Datacenter routing by default, works out of the box.
- 🧾 **Honest dates.** A search result only ever carries relative text like "6 years ago", never an exact timestamp, so this actor gives you that text plus a derived date flagged as approximate instead of pretending to know the real one.

### What data can you extract?

| Field | Type | Description |
|---|---|---|
| `position`, `keyword`, `rowType` | number, string, string | Rank for this keyword, the keyword that produced the row, and the kind of row: video, channel, playlist or shelfItem |
| `title`, `url`, `videoId` | string | Result title, canonical link, and video ID when the row is a video |
| `channel.name`, `channel.id`, `channel.url`, `channelHandle`, `channelIsVerified` | string, string, string, string, boolean | The channel behind the result, its @handle, and its verified badge |
| `subscriberCount`, `subscriberCountText` | number, string | Subscriber count, populated on channel rows |
| `viewCount`, `viewCountText`, `concurrentViewers` | number, string, number | Lifetime view count and its text, plus a live stream's current watcher count kept in its own field, never folded into `viewCount` |
| `publishedText`, `publishedAt`, `publishedAtIsApproximate` | string, date, boolean | The relative text YouTube shows, a date derived from it, and a flag marking that date as approximate. The text is kept word for word in whatever language you ran |
| `durationSeconds`, `durationText`, `isShort` | number, string, boolean | Length in seconds, the text as shown, and whether the result is a Short |
| `isLive`, `isUpcoming` | boolean | Whether the result is live now, or an upcoming premiere or scheduled stream |
| `descriptionSnippet` | string | The description preview shown under the result |
| `badges` | array | Labels YouTube attaches, such as Live now or New |
| `thumbnails` | array | Thumbnail image URLs |
| `playlistId`, `playlistItemCount` | string, number | Populated on playlist rows |
| `shelfTitle` | string | Names the shelf a `shelfItem` row came from |
| `estimatedResults` | number | YouTube's own total-match count for the keyword, a free signal for how competitive it is |
| `searchedAt`, `language` | date, string | When the search ran and which language it ran in, so rows from different runs stay comparable in one table |

### YouTube search scraper vs the official YouTube Data API

| Concern | Official YouTube Data API | This actor |
|---|---|---|
| Daily limit | 10,000 units a day by default; one keyword search costs 100 units, about 100 searches a day | No daily cap |
| Bulk keyword lists | One query per call, quota burns fast | Paste a full keyword list in one run |
| Getting more quota | Extension requests for scraping or competitive analytics are frequently rejected | Nothing to request |
| Rank position | Not returned | Every row carries its position (shelf rows, if you enable them, don't) |
| Setup | Google Cloud project, API key, OAuth on some endpoints | An Apify token, nothing else |
| Cost at volume | Free until quota, then blocked | $2 per 1,000 results |

If you need a handful of searches a day, the official API is free and fine. Past that, the quota is the wall this actor exists to remove.

### How to scrape YouTube search results: step by step

1. [Create a free Apify account](https://console.apify.com/sign-up). Takes 30 seconds, no card needed.
2. Open [YouTube Search Scraper](https://console.apify.com/actors/ge5BpC9ZGa5R3Thh9) in the Apify Console.
3. Paste your keywords, one per line, and set any filters you want: result type, sort, upload window, duration, features.
4. Click **Start**.
5. Export as JSON, CSV or Excel, or pull the dataset over the Apify API.

Or drive it from the command line:

```bash
curl -X POST "https://api.apify.com/v2/acts/tubelens~youtube-search-scraper/run-sync-get-dataset-items?token=APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keywords": ["ai agents", "cooking recipes"]}'
```

### How much does YouTube Search Scraper cost?

Two charges: **$0.005 per run start**, plus **$0.002 per result**, which works out to **$2 per 1,000 results**. Every row costs the same, whatever type it is. On the Apify **free plan** ($5 of monthly credit) that's about **2,500 results**. On **Starter** ($29), about **14,500**.

### Input

```json
{
  "keywords": ["ai agents", "cooking recipes", "react tutorial"],
  "maxResultsPerKeyword": 50,
  "resultType": "video",
  "sortBy": "relevance",
  "uploadedWithin": "any",
  "duration": "any",
  "features": [],
  "language": "en",
  "includeShelves": false
}
```

| Field | Type | Default | Notes |
|---|---|---|---|
| `keywords` | string\[] | required | One search term per line; every result row carries the keyword that produced it |
| `maxResultsPerKeyword` | integer | 50 | Caps rows returned per keyword, not how much the actor scans, 1 to 1,000 |
| `resultType` | enum | `video` | `video`, `channel`, `playlist`, `movie`, `shorts` |
| `sortBy` | enum | `relevance` | `relevance` or `popularity`, the only two sorts that actually work on YouTube's page |
| `uploadedWithin` | enum | `any` | `any`, `today`, `week`, `month`, `year` |
| `duration` | enum | `any` | `any`, `short` (under 3 min), `medium` (3 to 20 min), `long` (over 20 min) |
| `features` | string\[] | `[]` | `subtitles`, `4k`, `360`, `vr180`, `live` |
| `language` | string | `en` | Two-letter code; genuinely changes which results come back, not just labels. Publish dates are read in `ar`, `de`, `en`, `es`, `fr`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pt`, `ru` and `tr` |
| `includeShelves` | boolean | `false` | Off by default so you get only the organic, ranked results: anything YouTube mixes into the page without a rank, such as a shelf, a channel card or a catalogue film, is not returned and not billed. On, all of it comes back unranked and billed, and a video sitting in both the ranked list and a shelf comes back twice |

### Output

One row per search result, in rank order. A video row:

```json
{
  "rowType": "video",
  "videoId": "UyoVmQLekBc",
  "url": "https://www.youtube.com/watch?v=UyoVmQLekBc",
  "title": "How I Built an AI Agent in 14 minutes as A Beginner (2026)",
  "channel": {
    "name": "Mikey No Code",
    "id": "UCde0vB0fTwC8AT3sJofFV0w",
    "url": "https://www.youtube.com/@mikeynocode"
  },
  "viewCount": 19861,
  "viewCountText": "19,861 views",
  "concurrentViewers": null,
  "publishedText": "1 day ago",
  "publishedAt": "2026-07-27T00:00:00.000Z",
  "publishedAtIsApproximate": true,
  "durationSeconds": 1082,
  "durationText": "18:02",
  "thumbnails": [{ "url": "https://i.ytimg.com/vi/UyoVmQLekBc/hq720.jpg", "width": 720, "height": 404 }],
  "badges": ["New"],
  "isShort": false,
  "isLive": false,
  "isUpcoming": false,
  "descriptionSnippet": "Claim your FREE $499 Masterclass: Build & Sell Apps, AI Agents & Websites with AI ...",
  "channelHandle": null,
  "subscriberCount": null,
  "subscriberCountText": null,
  "playlistId": null,
  "playlistItemCount": null,
  "shelfTitle": null,
  "channelIsVerified": true,
  "position": 1,
  "keyword": "ai agents",
  "estimatedResults": 2411581,
  "searchedAt": "2026-07-28T20:27:15.782Z",
  "language": "en"
}
```

Fields that only apply to another row type come back `null`: `playlistId` and `playlistItemCount` populate on playlist rows, `channelHandle`, `subscriberCount` and `subscriberCountText` populate on channel rows, and `shelfTitle` names the shelf or Shorts grid a row sat in. `concurrentViewers` is deliberately separate from `viewCount`: a live stream's current watcher count is not a lifetime view count, so live rows populate one and not the other.

With `includeShelves` off, every row you get is a ranked result, so `position` is never null. Turn shelves on and you also get everything YouTube mixed into the page without a rank: `rowType: "shelfItem"` rows from a shelf, a channel card injected between two videos, and catalogue films from YouTube Movies. All of those come back with `position: null`, and all of them are billed. Two things to budget for: a video can sit in both the ranked list and a shelf, and you get it twice, once ranked and once as a `shelfItem` (measured on one 60-row run, 3 videos arrived twice), and the catalogue films carry no view count and no publish date. Dedupe on `videoId` if you only want one copy, and keep the row whose `position` is not null.

Movie searches return full films alongside ordinary long videos, all with `rowType: "video"`. Films carry no view count and no publish date anywhere in YouTube's search response, so those two fields come back `null` on them rather than guessed.

### Which tubelens scraper do I need?

| I want to... | Use |
|---|---|
| Search YouTube by keyword and get ranked results with position | **This actor**, youtube-search-scraper |
| Pull everything from one channel: videos, Shorts, live streams, playlists, community posts | youtube-channel-scraper |
| Get one video's full metadata, transcript, comments and channel data | youtube-video-scraper |

They chain: search finds it, the channel scraper catalogs it, the video scraper reads it.

### Use cases

- 📊 **Rank tracking**: monitor where a video or channel sits for target keywords over time, on a schedule.
- 🔍 **Competitive research**: see which competitors' videos outrank yours for the same searches.
- 🧭 **Content gap analysis**: sweep a batch of keywords and compare `estimatedResults` across them to spot under-served searches.
- 📈 **Title and thumbnail testing**: check whether a change actually moves position.
- 🌍 **Localized research**: run the same keyword in different languages and see how the ranking shifts.
- 🥇 **Agency reporting**: pull rank data across many keywords for many clients into your own warehouse instead of a per-creator dashboard tool.
- 🎬 **Shorts strategy**: filter to Shorts only and see what's currently ranking in that format.

### Limitations

- **Depth depends mostly on the keyword, and it is not a fixed number.** Runs capped at 1,000 results, measured on the live actor: "how to tie a tie" reached 505 unique results, "best laptop 2026" 459, "ai agents" 443, "yoga for beginners" 318, and the very broad "music" exhausted at 93. Specific, multi-word keywords go deep; single broad terms run out fast, because YouTube itself stops serving more. The same keyword also varies run to run: two walks of "salesforce" returned 316 and 273. Filtering to `resultType: video` reaches deeper than an unfiltered search. When a run stops early for any reason other than running out of results, the status message says so.
- **Publish dates are approximate.** Search results carry no absolute timestamp, only text like "6 years ago". `publishedAt` is derived from that text and flagged with `publishedAtIsApproximate: true`.
- **Sort is Relevance or Popularity only.** Sorting by upload date or rating doesn't actually reorder anything on YouTube's own search page, so those options aren't offered. Use the `uploadedWithin` filter for recency instead.
- **One result type per run.** Set `resultType` to video, channel, playlist, movie or Shorts. YouTube does slip other shapes into a filtered page, a channel card or a catalogue film in the middle of a video search, and those are dropped rather than mixed into your rows, so a run returns the one type you asked for.
- **Only ranked results are returned and billed.** `position` runs 1, 2, 3 and so on down the results YouTube ranked for your search, whichever result type you asked for. Anything the page carries without a rank never takes a number away from a real result: by default it isn't returned at all, and it only arrives, unranked and billed, if you turn `includeShelves` on.
- **A `duration: short` search can include rows with no length.** Live streams have no fixed length, but YouTube's own filter still admits them into that bucket, so those rows report `durationSeconds: null` rather than a guess.
- **Country routing isn't offered.** Tested and dropped: result overlap between the datacenter default, `country: US` and `country: DE` measured 48 to 70 percent, but a same-settings datacenter run repeated against itself overlapped only 64 percent, so the country effect was indistinguishable from ordinary run-to-run noise, even with residential proxy genuinely engaged. `language` is what actually re-ranks results, so that's the lever this actor exposes.
- **A run that stops early says `Partial:` and names the cause.** Stopping early means something cut the run short: the time budget, a charge limit, a page ceiling, or YouTube handing back a page that couldn't be read. Asking for more results than a keyword actually has is not stopping early; that run simply returns everything that exists and reports the count plainly.
- **Publish dates need a language this actor can read.** `publishedAt` is derived from YouTube's relative wording, which is written in the language you run in. It is read in `ar`, `de`, `en`, `es`, `fr`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pt`, `ru` and `tr`. Run in any other language and every other field still arrives, `publishedText` still carries YouTube's own wording, and the run warns in its log that `publishedAt` will be empty rather than leaving you to find out from the data.
- **This actor doesn't read the videos it finds.** Transcripts, comments and full descriptions come from `youtube-video-scraper`. A channel's full catalog comes from `youtube-channel-scraper`.

### FAQ

**How much does YouTube Search Scraper cost?**
YouTube Search Scraper uses pay-per-result pricing. You pay **$2 for 1,000 results**, plus a small $0.005 run-start fee. The Apify Free plan gives you $5 in usage credits a month, enough for around **2,500 results**. If you run regularly, the $29/month Starter plan covers about **14,500 results**.

No subscription lock-in. Pause whenever.

**How deep does search go?**
Deeper than the first page, but it isn't unlimited, and how deep depends mostly on the keyword. Measured on the live actor with the cap at 1,000: "how to tie a tie" reached 505 unique results, "best laptop 2026" 459, "ai agents" 443, "yoga for beginners" 318, and the very broad "music" ran out at 93. Specific multi-word keywords go deep, single broad terms exhaust fast. The same keyword varies between runs too: two walks of "salesforce" returned 316 and 273. Filtering to `resultType: video` reaches deeper than a broad, unfiltered search.

**Is it legal to scrape YouTube?**
Scraping public data is generally allowed in the US and most of the EU, as long as you don't collect personal data covered by GDPR or CCPA without a lawful basis. This actor only touches YouTube's public search results, but how you use the output is on you.

Apify's full breakdown: [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/).

**Can I integrate YouTube Search Scraper with other tools?**
Push results into **Make, Zapier, Slack, Airbyte, GitHub, Google Sheets, Google Drive**, and more. Apify treats every actor as a webhook source, so anything that consumes webhooks or pulls from an API works.

Full list: [Apify integrations](https://docs.apify.com/platform/integrations).

**Can I use YouTube Search Scraper with the Apify API?**
Yes. Every run is available over the Apify REST API:

```bash
curl -X POST "https://api.apify.com/v2/acts/tubelens~youtube-search-scraper/runs?token=APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keywords": ["ai agents"]}'
```

Docs: [Apify API reference](https://docs.apify.com/api/v2).

**Can I use YouTube Search Scraper through an MCP Server?**
Yes. Apify ships an MCP server that exposes every actor as a tool, so Claude Desktop, Cursor, and any other MCP-capable client can call YouTube Search Scraper. Setup: [Apify MCP docs](https://docs.apify.com/platform/integrations/mcp).

**Your feedback**
Found a bug or need a field that isn't here? Open an issue at [console.apify.com/actors/ge5BpC9ZGa5R3Thh9/issues](https://console.apify.com/actors/ge5BpC9ZGa5R3Thh9/issues) and it goes straight to a human.

# Actor input Schema

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

Search terms to run on YouTube. Each result row carries the keyword that produced it. Paste one per line.

## `maxResultsPerKeyword` (type: `integer`):

How many rows to return per keyword. This caps the rows returned, not how many results the actor scans.

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

Which kind of result to search for.

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

How YouTube orders the results. Only Relevance and Popularity are real, working sorts on YouTube's own search page, so those are the only two offered here.

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

Only return results uploaded within this time window.

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

Only return results in this length range: short is under 3 minutes, medium is 3 to 20 minutes, long is over 20 minutes.

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

Only return videos with these properties. Leave empty for no restriction.

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

Language YouTube uses to rank results, as a two-letter code such as en or hi. This genuinely changes which results come back, not just how they are labelled. Publish dates are read in ar, de, en, es, fr, hi, id, it, ja, ko, nl, pt, ru and tr; any other language still returns every other field, keeps YouTube's own date wording in publishedText, and warns in the run log that publishedAt will be empty.

## `includeShelves` (type: `boolean`):

YouTube mixes extra shapes into the results page: shelves, grids, a channel card between two videos, catalogue films. Off by default so you only get the organic, ranked search results, and anything without a rank is neither returned nor billed. Turn it on and all of it comes back with position null and is billed, and a video that appears both in the ranked list and inside a shelf comes back as two rows, one ranked and one marked shelfItem.

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

Datacenter proxy is used by default and is enough for YouTube. Residential is used automatically if a datacenter address gets blocked.

## Actor input object example

```json
{
  "keywords": [
    "ai agents",
    "cooking recipes",
    "react tutorial"
  ],
  "maxResultsPerKeyword": 50,
  "resultType": "video",
  "sortBy": "relevance",
  "uploadedWithin": "any",
  "duration": "any",
  "features": [],
  "language": "en",
  "includeShelves": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Ranked search results for each keyword.

# 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": [
        "ai agents"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tubelens/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 = { "keywords": ["ai agents"] }

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

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    "ai agents"
  ]
}' |
apify call tubelens/youtube-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=tubelens/youtube-search-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/ge5BpC9ZGa5R3Thh9/builds/qPXSf24ResCVMBX55/openapi.json
