# YouTube Search Scraper · SERP Rank Tracker & Keywords (`thequietstack/youtube-search-tracker`) Actor

YouTube search scraper and rank tracker for keyword research and SEO monitoring: search results with their exact position, the rank change since your last run and an only-new mode. Track your own channels or videos per keyword. No login, no API key.

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

## Pricing

from $0.70 / 1,000 search 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 Scraper · SERP Rank Tracker & Keyword Research

**YouTube SERP rank tracker and keyword research:** search results with exact positions, rank changes since the last run and a report per keyword.

YouTube search results with their **exact position** - and what changed since your last run. Track where your channel or your videos rank for any keyword (and pay only for those rows), get exact view counts, optional likes and comment counts, and one **keyword report** per query: how big the views of the top results are, how fresh they are, how many titles use the keyword, and which channels own the page.

No login, no Google account, no API key, no YouTube Data API quota.

### What you get

| | This Actor | Typical YouTube search scrapers |
|---|---|---|
| Position | `rank` (as shown), `typeRank` (among videos only), `page` | a row number, if any |
| Rank tracking | `trackChannels` / `trackVideos`; `outputMode: "trackedOnly"` writes only your rows plus "not in top N" | scrape everything, filter yourself |
| Change since last run | `previousRank`, `rankChange` (+3 = up three places), `isNew` | - |
| Monitoring | `onlyNewResults`: the next run returns only results you have not received yet | same results again, charged again |
| Noisy order handled | `rankSamples: 3` -> median position, best/worst, seen in k of 3 | single snapshot |
| Views | exact (`751,311`) for videos; Shorts flagged `viewsExact: false` or made exact with details | "751K" silently turned into 751000 |
| Likes / comments / date | `enrichVideoDetails` (exact where YouTube shows them exactly) | often missing |
| Keyword report | view distribution, freshness, title match, top channels | - |
| Filters before charging | min views, max age, title words, all query words in title | rarely |
| Duplicates | removed across pages (YouTube repeats results on later pages) | often duplicated |

### Use cases

- **YouTube SEO rank tracking:** schedule a daily run with your keywords, `trackChannels: ["@yourchannel"]`, `outputMode: "trackedOnly"` and `rankSamples: 3`. You get one row per position of your videos and one report row per keyword - including "not in the top 50".
- **Keyword research before you film:** run 20 candidate keywords with `outputMode: "reportsOnly"` (one row per keyword). A keyword where the median top video has 2,000 views and 40 % of results are from the last 30 days is a different opportunity than one where the median is 2 million and nothing is newer than a year.
- **Competitor monitoring:** track three competitor channels across your niche keywords and see who moves up.
- **New-upload alerts for a topic:** `uploadDate: "today"` + `onlyNewResults: true` on a schedule -> each run delivers only new videos for your keywords, straight into Google Sheets, Slack, Make, n8n or Zapier.
- **Influencer and sponsor discovery:** `resultType: "channels"` for channel cards with subscriber counts, or videos with `minViews` to find who ranks.
- **Datasets for research or AI:** clean JSON per result with position, query and region.

### Input example

```json
{
    "queries": ["notion template", "notion dashboard"],
    "resultType": "videos",
    "region": "US",
    "maxResultsPerQuery": 50,
    "trackChannels": ["@ProductiveSetups"],
    "outputMode": "trackedOnly",
    "rankSamples": 3,
    "compareWithPreviousRun": true
}
```

- `queries` - search text, or a `youtube.com/results?search_query=...&sp=...` link (its filters are used as they are).
- `resultType` (`videos` default, `shorts`, `channels`, `playlists`, `all`), `sortBy` (`relevance`, `popularity`), `uploadDate` (`today`, `week`, `month`, `year`), `duration` (`under3`, `3to20`, `over20`), `features` (`live`, `4k`, `hd`, `subtitles`, `creativeCommons`, `360`, `vr180`, `3d`, `hdr`, `location`, `purchased`) - exactly YouTube's own filter menu.
- `region` - country of the searching visitor (ranks differ by country).
- `maxResultsPerQuery` (scan depth, default 50, max 1000), `maxPagesPerQuery`, `maxResults` (hard cap on rows per run), `rankSamples` (1-5).
- `outputMode` (`all`, `trackedOnly`, `reportsOnly`), `trackChannels`, `trackVideos`, `compareWithPreviousRun`, `onlyNewResults`, `stateStoreName`.
- `keywordReport`, `enrichVideoDetails`.
- Filters: `titleIncludes`, `titleExcludes`, `requireQueryWordsInTitle`, `minViews`, `maxAgeDays`, `dedupeAcrossQueries`; `requestDelaySecs`.

### Output example

Real rows from runs on 01 Oct 2026 (VPS test runs, region US unless noted).

A result with its change since the run four minutes earlier:

```json
{
    "recordType": "video",
    "query": "home workout no equipment",
    "rank": 4,
    "typeRank": 4,
    "page": 1,
    "inShortsShelf": false,
    "isTracked": false,
    "trackedBy": [],
    "previousRank": 12,
    "rankChange": 8,
    "isNew": false,
    "id": "KeNObkhENKQ",
    "url": "https://www.youtube.com/watch?v=KeNObkhENKQ",
    "title": "TOP 10 Exercises to Get Fit At Home!",
    "channelTitle": "Calisthenicmovement",
    "channelHandle": "@calimove",
    "channelId": "UCZIIRX8rkNjVpP-oLMHpeDw",
    "channelUrl": "https://www.youtube.com/@calimove",
    "channelVerified": true,
    "videoId": "KeNObkhENKQ",
    "views": 2041196,
    "viewsText": "2,041,196 views",
    "viewsExact": true,
    "liveWatching": null,
    "publishedTimeText": "3 years ago",
    "publishedAtLatest": "2023-10-02T15:35:36.197Z",
    "publishedAtEarliest": "2022-09-28T15:35:36.197Z",
    "durationText": "8:11",
    "durationSeconds": 491,
    "isLiveNow": false,
    "isUpcoming": false,
    "badges": ["4K"],
    "snippet": "Our Workout Programs https://calimove.com *Follow Along Routine* ...",
    "thumbnailUrl": "https://i.ytimg.com/vi/KeNObkhENKQ/hq720.jpg",
    "searchFilters": "EgIQAQ==",
    "region": "US",
    "scrapedAt": "2026-10-01T15:35:36.197Z"
}
```

Rank tracking with `rankSamples: 3` (tracked channel, `outputMode: "trackedOnly"`) - shortened:

```json
{ "query": "notion template", "rank": 1, "rankMin": 1, "rankMax": 2, "seenInSamples": 3, "samples": 3, "isTracked": true, "channelHandle": "@ProductiveSetups", "title": "Become a Notion Expert in 20 minutes | Notion Tutorial for Beginners (Full Guide)" }
```

A Short from the Shorts shelf with `enrichVideoDetails` (region DE) - shortened:

```json
{ "recordType": "short", "rank": 1, "inShortsShelf": true, "url": "https://www.youtube.com/shorts/s3mjR7ORVZM", "channelHandle": "@realsun12", "views": 550399, "viewsExact": true, "likes": 6710, "likesExact": true, "comments": 60, "commentsExact": true, "publishedDate": "2025-06-01", "publishedDateApprox": false, "hashtags": ["Notion", "Productivity", "NotionSetup"], "detailsStatus": "ok" }
```

A keyword report (one per query, `recordType: "keyword-report"`) - top channels shortened to three:

```json
{
    "recordType": "keyword-report",
    "query": "home workout no equipment",
    "region": "US",
    "youtubeEstimatedResults": 11395842,
    "resultsScanned": 25,
    "rankSamples": 1,
    "pagesFetched": 2,
    "counts": { "video": 25, "short": 0, "channel": 0, "playlist": 0 },
    "videoViews": { "sample": 25, "min": 292444, "p25": 1252654, "median": 2337966, "p75": 6575669, "max": 33372274, "exact": true },
    "freshness": { "videosWithAge": 25, "under7Days": 0, "under30Days": 0, "under365Days": 2, "shareUnder30Days": 0, "shareUnder365Days": 0.08 },
    "titleMatch": { "titles": 25, "shareWithExactPhrase": 0, "shareWithAllWords": 0.32 },
    "uniqueChannels": 15,
    "topChannels": [
        { "channelTitle": "MadFit", "channelHandle": "@MadFit", "results": 8, "bestRank": 7 },
        { "channelTitle": "growingannanas", "channelHandle": "@growingannanas", "results": 4, "bestRank": 1 },
        { "channelTitle": "Calisthenicmovement", "channelHandle": "@calimove", "results": 1, "bestRank": 4 }
    ],
    "tracked": [{ "tracked": "@productivesetups", "found": false, "bestRank": null, "notInTop": 25, "previousBestRank": null }],
    "comparedWithRunAt": "2026-10-01T15:31:31.319Z",
    "newSinceLastRun": 10,
    "goneSinceLastRun": 10
}
```

The run summary (`SUMMARY` in the key-value store) lists every query with its status (`ok`, `no_results`, `partial`, `failed`, `blocked`), results scanned, pages, rows written, what was skipped and why, and why the scan stopped.

### How much does it cost?

Pay per event:

| Event | Price | When |
|---|---|---|
| `search-result` | $0.001 | each result row written ($1.00 per 1,000) |
| `video-details` | $0.001 | extra, per row whose likes/comments/date were fetched (`enrichVideoDetails`) |
| `keyword-report` | $0.005 | each keyword report row |

| Job | Cost |
|---|---|
| 1,000 search results (20 queries x 50) | $1.00 |
| Same with likes, comments and exact dates | $2.00 |
| Daily rank check: 30 keywords, top 50, 3 samples, `trackedOnly`, ~1 hit per keyword | 30 x ($0.005 + $0.001) = $0.18 per day |
| Keyword research: 50 candidate keywords, `outputMode: "reportsOnly"` | 50 x $0.005 = $0.25 |

Never charged: results scanned only to compute ranks or reports, results outside `trackedOnly`, filtered results, results already delivered by `onlyNewResults`, duplicates, watch data that was unavailable or failed, queries with no results, failed or blocked queries. Set *Max cost per run* in Apify and the run stops cleanly at that amount; `maxResults` is a second hard cap.

### Honest numbers

- **YouTube's order is not stable for logged-out visitors.** Measured on 01 Oct 2026 with four identical requests per query: the top 20 shared 9-20 of 20 videos, but in the top 10 only 1-8 videos kept their exact position. A single snapshot can move a video several places without anything changing. For tracking, use `rankSamples: 3`: you get the median position, best and worst, and in how many samples the result appeared. Also expect `newSinceLastRun` to be non-zero even minutes apart.
- **Ranks are positions in what YouTube returned**, counted over every result in reading order (each Short of a Shorts shelf counts). `typeRank` counts only results of the same type - the number most rank trackers mean. Ads, "People also watched" and search suggestions are not results and are skipped. Filters never change ranks.
- **Views** of normal videos are exact in the results list. Shorts show only rounded counts there ("550K") - `viewsExact: false` - and become exact with `enrichVideoDetails`. Live streams have `liveWatching` instead of views.
- **Ages** are YouTube's "3 weeks ago"; you get that text plus `publishedAtLatest` / `publishedAtEarliest`. Exact dates come with `enrichVideoDetails`. `maxAgeDays` drops a video only when it is certainly older.
- **Likes and comment counts** (details) are exact when YouTube shows them exactly and flagged otherwise.
- **`youtubeEstimatedResults`** is YouTube's own estimate, not a count.
- Texts are requested in English so numbers and dates are read exactly; queries can be in any language, and `region` decides the ranking. Shorts in a mixed results page show no channel: tracking them needs `enrichVideoDetails`.

### Polite access - Bot protection is never bypassed

- Only YouTube's public web-client data for the results page and (with details) the watch page - the same requests youtube.com makes for a visitor who is not logged in. The video player / stream endpoint is never called, nothing is downloaded.
- One request at a time with a pause (`requestDelaySecs`, default 1 s), hard caps per query and per run, filters applied before charging.
- **Bot protection is never bypassed.** If YouTube answers with HTTP 403 or 429, a captcha or "confirm you're not a bot" page, a consent or login redirect, or an answer without the expected structure, the Actor stops the whole run at once with status FAILED and a clear message (`SUMMARY.blocked` names where and why; remaining queries are listed under `notReached`). No retries of a block, no proxy or IP rotation, no header tricks, no login, no cookies, no consent clicking. Results read before the stop are written; the blocked query gets no keyword report and no memory update, and nothing is charged for what was not delivered.
- A plain server error (5xx) is retried exactly once after 5 seconds; anything else is reported, not retried.

### FAQ

**Do I need a YouTube API key?** No, and there is no daily quota like the YouTube Data API's 100 searches per day.

**How deep can it go?** As far as YouTube lets a visitor scroll; capped by `maxResultsPerQuery` (max 1000) and `maxPagesPerQuery`. Later pages repeat earlier results - duplicates are removed and not charged.

**Why do I get fewer rows than results scanned?** `trackedOnly`, filters, `onlyNewResults` and `maxResults`. The summary lists every skip reason.

**Can I use my own filter combination from the browser?** Yes - paste the results link (with `&sp=...`) as a query.

**Does it get e-mails or contact data?** No. It returns what YouTube shows publicly in search results: titles, channels, counts.

**Is it legal?** It reads public pages without logging in. Respect YouTube's Terms of Service and creators' rights when you republish content. This Actor is not affiliated with or endorsed by YouTube or Google.

### Use it via API, integrations and AI agents

```bash
curl -X POST "https://api.apify.com/v2/acts/thequietstack~youtube-search-tracker/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"queries":["notion template"],"maxResultsPerQuery":50,"keywordReport":true}'
```

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("thequietstack/youtube-search-tracker").call(
    run_input={"queries": ["notion template", "notion dashboard"], "trackChannels": ["@yourchannel"],
               "outputMode": "trackedOnly", "rankSamples": 3, "compareWithPreviousRun": True}
)
for r in client.dataset(run["defaultDatasetId"]).iterate_items():
    if r["recordType"] == "keyword-report":
        print(r["query"], [(t["tracked"], t["bestRank"], t["previousBestRank"]) for t in r["tracked"]])
```

Works with Apify schedules, webhooks and integrations (Google Sheets, Make, n8n, Zapier, Slack), and as a tool for AI agents through the Apify MCP server.

### Works with

- [YouTube Channel Videos Scraper](https://apify.com/thequietstack/youtube-channel-videos) - every upload of the channels you found, with exact views, likes and dates.

# Actor input Schema

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

What you would type into YouTube's search box, one per line - or a youtube.com/results?search_query=... link (its filters, the sp= part, are used as they are). Up to 200 queries per run.

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

YouTube's own type filter. 'All' returns the mixed page as a visitor sees it: videos, the Shorts shelf, playlists and channels, each with its position.

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

The two orders YouTube's filter menu offers (checked 01 Oct 2026; the old 'Upload date' sort is no longer offered - use 'Upload date' below plus onlyNewResults for fresh uploads).

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

YouTube's upload-date filter.

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

YouTube's duration filter.

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

YouTube's feature filters (all selected must apply).

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

Two-letter country code of the searching visitor (US, GB, DE, IN, BR ...). Ranks differ by country. The interface language is always English, so numbers and dates are read exactly; queries can be in any language.

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

How many results of each query are read, in YouTube's order (the first page has ~20, later pages up to 100). Ranks are positions within this depth.

## `maxPagesPerQuery` (type: `integer`):

Hard cap on result pages per query.

## `maxResults` (type: `integer`):

Hard cap on result rows written (and charged) in this run. Keyword reports are not counted here.

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

'Only tracked' scans every query to the full depth but writes (and charges) only the rows of your tracked channels or videos, plus one keyword report per query that says where they rank - or that they are not in the top N. 'Only keyword reports' writes one report row per query and no result rows.

## `trackChannels` (type: `array`):

Your channel or competitors: @handle, youtube.com/@handle or a UC... channel ID. Their results are marked isTracked and their best position per query goes into the keyword report. Note: Shorts in a mixed results page show no channel - add enrichVideoDetails to match them.

## `trackVideos` (type: `array`):

Video links or IDs whose position you want per query.

## `rankSamples` (type: `integer`):

YouTube's results order for logged-out visitors changes between identical searches (measured 01 Oct 2026: in the top 10 only 1-8 of 10 videos kept their exact position from one request to the next). With 3, each query is searched three times and every result gets its median position, best and worst position and in how many samples it appeared. Costs no extra events, only time.

## `compareWithPreviousRun` (type: `boolean`):

Remembers each query's ranks in a named key-value store. Every row then carries previousRank, rankChange (+3 = moved up three places) and isNew; the keyword report counts new and vanished results.

## `onlyNewResults` (type: `boolean`):

For scheduled keyword monitoring: results already delivered by an earlier run of the same query (same filters and region) are skipped and not charged. Implies 'Compare with the previous run'.

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

Named key-value store for the comparison memory. Use one name per project or schedule.

## `keywordReport` (type: `boolean`):

Adds one row per query (recordType 'keyword-report'): YouTube's result estimate, view distribution of the top results (exact), share of fresh uploads, how many titles contain the keyword, top channels, positions of tracked channels/videos. Always on in 'Only tracked' mode.

## `enrichVideoDetails` (type: `boolean`):

One extra request per written video or Short (the public watch-page data): likes (exact where YouTube shows them exactly), comment count, publish date, description, hashtags, and channel and views for Shorts. Charged as an extra event per row; slower.

## `titleIncludes` (type: `array`):

Write only results whose title contains at least one of these words or phrases (case-insensitive). Ranks still count every result.

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

Skip results whose title contains any of these.

## `requireQueryWordsInTitle` (type: `boolean`):

Skip results whose title does not contain every word of the query. YouTube fills thin queries with loosely related videos; this keeps only exact topical matches.

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

Skip videos with fewer views. Rounded counts (Shorts: '550K') are compared by their highest possible value, so nothing that could qualify is dropped.

## `maxAgeDays` (type: `integer`):

Skip videos that are certainly older (by YouTube's '3 weeks ago' or, with details, by the exact date). Items without a shown age are kept.

## `dedupeAcrossQueries` (type: `boolean`):

By default a video that ranks for two queries appears (and is charged) once per query, with each rank. Turn on to keep only its first appearance.

## `requestDelaySecs` (type: `integer`):

One request at a time with this pause. Raise it for very large runs.

## Actor input object example

```json
{
  "queries": [
    "notion template",
    "home workout no equipment"
  ],
  "resultType": "videos",
  "sortBy": "relevance",
  "uploadDate": "any",
  "duration": "any",
  "features": [],
  "region": "US",
  "maxResultsPerQuery": 50,
  "maxPagesPerQuery": 20,
  "maxResults": 1000,
  "outputMode": "all",
  "trackChannels": [
    "@ProductiveSetups"
  ],
  "trackVideos": [
    "https://www.youtube.com/watch?v=92LK3J0ZykA"
  ],
  "rankSamples": 1,
  "compareWithPreviousRun": false,
  "onlyNewResults": false,
  "stateStoreName": "youtube-search-tracker-state",
  "keywordReport": false,
  "enrichVideoDetails": false,
  "requireQueryWordsInTitle": false,
  "dedupeAcrossQueries": false,
  "requestDelaySecs": 1
}
```

# Actor output Schema

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

One row per search result (recordType video, short, channel or playlist) with query, rank, rank in its type, page, previous rank and change, tracked flag, ID and link, title, channel, views (with exact flag), age, length, badges; optionally likes, comments, exact date and description. Keyword-report rows (recordType keyword-report) summarise each query.

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

Per query: status (ok, no_results, partial, failed, blocked), results scanned, pages, rows written, what was skipped and why, why the scan stopped; HTTP statistics and billing. Nothing listed as skipped is 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": [
        "notion template"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("thequietstack/youtube-search-tracker").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": ["notion template"] }

# Run the Actor and wait for it to finish
run = client.actor("thequietstack/youtube-search-tracker").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": [
    "notion template"
  ]
}' |
apify call thequietstack/youtube-search-tracker --silent --output-dataset

```

## MCP server setup

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

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/gjBcC6Qjn4D8mmxKA/builds/ZLfOYWmQEORId1cH5/openapi.json
