# YouTube Search Scraper — All Filters, Video & Channel Data (`scrapersdelight/youtube-search-scraper`) Actor

Scrape YouTube search results by keyword with every filter YouTube offers: 4 sort orders, 6 upload windows (from last hour), 6 result types, 4 durations, 11 feature flags. Each row: video ID, title, description snippet, channel name/ID/handle, views, duration, thumbnails, badges, chapters.

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

## Pricing

$0.80 / 1,000 per search result returneds

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/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 — every filter, every field in one request

Scrape **YouTube search results by keyword** and get back the whole row YouTube's own search
response contains: video ID, title, description snippet, channel name **and channel ID and @handle**,
view count, duration, thumbnail ladder, badges, and **per-video chapters** — plus the complete
filter surface (4 sort orders × 6 upload windows × 6 result types × 4 durations × 11 feature flags).

No API key. No login. No cookies. No browser. **One HTTP request per page of results.**

***

### 🚀 What does YouTube Search Scraper do?

You give it keywords. It runs them through YouTube's own internal search endpoint, walks the
continuation chain, de-duplicates, and writes one row per result to your dataset.

- 🎬 **Videos** — the full result row, in YouTube's own ranking order (`searchPosition`)
- 📺 **Channels** — `resultType: channel` returns channel rows with ID, @handle, subscribers, bio
- 📚 **Playlists** — ID, owner, video count, preview titles, first video
- 🍿 **Movies** — genre, release year, cast, director, runtime
- ⚡ **Shorts** — including the uploader's **channel ID**, which YouTube does not put in a Shorts row
  at all (recovered from the reel endpoint — see *The wedges*, below)
- 🔴 **Live streams** — with the live viewer count, correctly separated from view count
- 🧭 **Chapters** — title, timestamp and `startSeconds`, straight off the search response,
  **without a second request per video**

#### 📊 Every field on a video row

| Field | What it is | Fill on the 600-row proof run |
|---|---|---|
| `type` | `video` / `short` / `channel` / `playlist` / `movie` | 100.0% |
| `videoId` | YouTube's 11-char ID | 100.0% |
| `url` | `https://www.youtube.com/watch?v=…` | 100.0% |
| `title` | Video title | 100.0% |
| `titleAccessibilityLabel` | Spoken title incl. duration | 100.0% |
| `descriptionSnippet` | The search snippet, query terms included | 99.7% |
| `descriptionSnippetHoverText` | What the snippet was taken from | 99.7% |
| `channelName` | Uploader name | 100.0% |
| `channelId` | `UC…` channel ID | 100.0% |
| `channelHandle` | `@handle` | 99.8% |
| `channelUrl` | Canonical channel URL | 100.0% |
| `channelVerified` | Verified badge present | 100.0% (true on 273) |
| `channelBadges` | Verified / Official Artist Channel | 45.7% |
| `channelAvatarUrl` | Largest avatar | 100.0% |
| `channelAvatarThumbnails` | Full avatar ladder `{url,width,height}` | 100.0% |
| `collaborators` | Every co-uploader of a collab video, with ID + handle + subscribers | 0.3% |
| `viewCountText` | `"3,315,126 views"` | 100.0% |
| `viewCount` | Integer `3315126` (`"No views"` → `0`) | 99.8% |
| `shortViewCountText` | `"3.3M views"` | 100.0% |
| `liveViewerCountText` | `"15 watching"` — live rows only | 0.2% |
| `liveViewerCount` | Integer live viewers | 0.2% |
| `publishedTimeText` | `"6 years ago"` / `"Streamed 1 day ago"` | 99.8% |
| `publishedTimeApprox` | ISO date derived from that — **approximate, see limits** | 99.8% |
| `durationText` | `"18:37"` | 99.8% |
| `durationSeconds` | Integer `1117` | 99.8% |
| `durationAccessibilityLabel` | `"18 minutes, 37 seconds"` | 99.8% |
| `isLive` / `isShort` / `isUpcoming` | Row-kind flags | 100.0% |
| `scheduledStartTime` | ISO start of an upcoming premiere | 0.0% on this run |
| `badges` | `New` / `CC` / `4K` / `LIVE` labels | 48.7% |
| `hasCaptions` | CC badge present | 100.0% (true on 76) |
| `is4K` | 4K badge present | 100.0% (true on 244) |
| `thumbnails` | Full ladder, 360×202 + 720×404 | 100.0% |
| `thumbnailUrl` | Largest thumbnail | 100.0% |
| `movingThumbnailUrl` | Animated hover preview (.webp) | 77.5% |
| `chapters` | `[{title, time, startSeconds}]` | 31.7% (190 rows, **1,811 chapters**) |
| `chaptersCount` | Integer | 100.0% |
| `playerParams` | YouTube's player params for the row | 100.0% |
| `searchVideoResultEntityKey` | Stable per-result tracking key | 100.0% |
| `inlinePlaybackUrl` | Inline-preview watch URL | 97.0% |
| `searchQuery` | Which of your keywords produced this row | 100.0% |
| `searchPage` | Continuation page index | 100.0% |
| `searchPosition` | Rank inside the result set — YouTube's own ordering | 100.0% |
| `estimatedResults` | YouTube's estimate for the whole query | 100.0% |
| `scrapedAt` | ISO timestamp of the scrape | 100.0% |

**46 fields on a video row; 28 of them at 100.0% fill across the 600-row proof run.**

#### Other row types (measured)

| Type | Rows measured | Fields | Notable |
|---|---|---|---|
| `channel` | 80 | 23 | `channelId` 100%, `@handle` 100%, `subscriberCount` 98%, `descriptionSnippet` 90% |
| `playlist` | 40 | 21 | `playlistId`, owner ID + handle, `videoCount`, `previewVideoTitles`, `firstVideoId` — all 100% |
| `movie` | 19 | 32 | `releaseYear` 100%, `genre` 100%, `cast` 100%, `director` 89% |
| `short` | 51 | 46 | `videoId` 100%, `viewCount` 100%, **`channelId` 100%** |

***

### 🎛️ Every filter YouTube has — not a hand-picked subset

Most YouTube search scrapers ship a fixed lookup table of pre-baked filter strings, so you get
whatever combinations somebody typed out by hand. This actor **encodes YouTube's search protobuf
properly**, so the entire cross-product is reachable:

| Filter | Values |
|---|---|
| `sortBy` | `relevance` · `uploadDate` · `viewCount` · `rating` |
| `uploadDate` | `any` · **`lastHour`** · `today` · `thisWeek` · `thisMonth` · `thisYear` |
| `resultType` | `video` (default) · `any` · `shorts` · `channel` · `playlist` · `movie` |
| `duration` | `any` · `under3min` · `from3to20min` · `over20min` |
| `features` | `live` · `fourK` · `hd` · `subtitles` · `creativeCommons` · `threeSixty` · `vr180` · `threeD` · `hdr` · `location` · `purchased` |

**And it checks itself.** Every search response carries YouTube's own filter chips with their
official encodings. On the first page of every run the actor decodes those chips and compares them
to its own encoder. If YouTube ever renumbers an enum, the actor adopts YouTube's value, logs the
drift, and re-runs the query — instead of silently handing you unfiltered results.

Measured on the proof runs: **24 of 24 encodings byte-identical to YouTube's own chips**
(`type=video` → `EgIQAQ==`, `duration>20m` → `EgIYAg==`, `VR180` → `EgPQAQE=`, …), and the actor
logged `encoder confirmed against 23 live filter chips` on every platform run.

***

### 🧩 The wedges — what this does that the search response does not hand you

1. **Shorts get a channel.** YouTube's `shortsLockupViewModel` has a title, a view count and a
   thumbnail, and nothing else — no channel name, no ID, no handle. The uploader's `UC…` ID is
   embedded in the reel endpoint's params protobuf, so it is recovered there. Verified against the
   videos' own watch pages: **4 of 4 exact matches**, then **51 of 51 Shorts rows** on the platform.
2. **Collaboration videos keep their channel.** When two creators co-upload, YouTube replaces the
   byline link with a "Collaborators" dialog — a parser that only reads the byline loses `channelId`
   *and* `channelHandle` on those rows. Here they are read from the dialog, and every collaborator
   ships in `collaborators[]` with ID, handle and subscriber count.
3. **Chapters without a second request.** 1,811 chapters across 190 of 600 rows, from the same
   response — no extra fetch, no extra latency, no extra charge.
4. **`"No views"` is `0`, not null.** A brand-new upload has a real, meaningful zero.
5. **`"15 watching"` is never mis-read as a view count.** It lands in `liveViewerCount`.

***

### 📥 Input

```json
{
  "searchQueries": ["plumber near me", "best crm software"],
  "resultType": "video",
  "sortBy": "relevance",
  "uploadDate": "any",
  "duration": "any",
  "features": [],
  "maxResultsPerQuery": 120,
  "maxResults": 0,
  "language": "en",
  "region": "US",
  "deduplicateAcrossQueries": false,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

| Input | Default | Notes |
|---|---|---|
| `searchQueries` | `["web scraping"]` | One per line. Volume comes from **many queries**, not one deep one — see the ceiling below. |
| `resultType` | `video` | `video` returns only videos. `any` reproduces youtube.com, Shorts shelf included — you pay for those rows too. |
| `sortBy` / `uploadDate` / `duration` / `features` | unfiltered | Server-side, combinable. |
| `maxResultsPerQuery` | `200` | Per-query stop. |
| `maxResults` | `0` (unlimited) | Overall stop across every query. |
| `language` / `region` | `en` / `US` | `hl` / `gl`. YouTube ranks per country — this is how you scrape the German result set for the same keyword. |
| `deduplicateAcrossQueries` | `false` | Duplicates **inside** a query are always dropped before billing. This also drops a result that already appeared under a different keyword. |
| `proxyConfiguration` | Apify Proxy `auto` | Verified HTTP 200 through `auto`, through residential, and from a home IP. |

***

### 💵 Pricing

**Pay per event — one event, nothing else.** No per-run start fee, no auto-events.

| Event | Price |
|---|---|
| `search-result-scraped` — per search result returned | **$0.0008** = **$0.80 / 1,000 results** |

Billing runs through `Actor.pushData(rows, 'search-result-scraped')`, which is budget-aware:
**delivered == billed**. If you set a charge cap, you are never handed rows you did not pay for, and
never billed for rows you did not get. Duplicates from YouTube's continuation chain are removed
**before** they are pushed, so you do not pay for repeats.

Measured on the platform: the 600-row run reported `chargedEventCounts: {"search-result-scraped": 600}`
against a dataset of exactly 600 items.

**Honest comparison** — do not take a blanket "cheapest" claim from anybody, including us:

- vs `api-ninja/youtube-search-scraper` **free tier** ($0.0025/result **+ a per-run fee**): we are
  **3.1× cheaper per result and charge no run fee**.
- vs the same actor's **paid tiers** ($0.00025/result on Bronze and above): **they are cheaper.**
  If you are on a paid Apify plan and only want their field set, they cost you less per row. What
  you get here instead is the wider row, the complete filter cross-product, and one flat rate.
- vs `khadinakbar/youtube-search-scraper` ($0.003/result): we are 3.75× cheaper.

***

### 📈 Measured performance (real runs on Apify, 2026-09-02)

| Run | Config | Result |
|---|---|---|
| Proof run | 5 queries × 120, `type=video`, Apify Proxy auto | **600 rows, 600 unique**, 69.5s, 35 requests, **100% success**, 0 blocks |
| Depth run | 2 queries, `maxResultsPerQuery: 1000` | 820 rows, 51 requests, **100% success** — both queries exhausted their continuation chain |
| Mixed | `resultType: any` | 80 rows: 51 Shorts + 28 videos + 1 channel |
| Channels / Playlists / Movies / Shorts | one run each | 80 / 40 / 19 / 40 rows, all types parsed |
| Filter proof | `sortBy: uploadDate` + `uploadDate: lastHour` + `hd` | 40 rows, freshest `"21 minutes ago"` |
| Health (prefill input) ×3 | | SUCCEEDED in **7.3s / 30.4s / 6.0s**, 50 rows each |
| Empty input `{}` ×2 | | SUCCEEDED in 13.3s / 23.9s, 200 rows each — never fails on empty input |

**Sustained-load check:** 86 consecutive InnerTube requests across the two deep runs, through Apify
Proxy, **zero non-200 responses and zero 403/429**. Requests are logged with a running success rate,
so you can see it yourself in any run.

***

### ⚠️ Honest limits — read these before you buy

- **YouTube caps how deep one query goes.** Measured to exhaustion on the platform: `"plumber"` ran
  dry at **473 unique results / 31 pages**, `"best crm software"` at **347 / 20 pages**. Then the
  continuation token goes null and the query ends (the log says `no further pages`). This is
  YouTube's ceiling, not this actor's — **every** YouTube search scraper hits it. Plan for volume by
  running **many queries and filter variants**, not one enormous one.
- **YouTube's continuation chain repeats itself.** Measured 151 duplicates in 971 raw rows (15.6%)
  on the depth run. They are dropped by ID before `pushData`, so you are never billed for them —
  but it is why "20 results per page × 30 pages" does not equal 600 unique rows.
- **The publish date is relative, not absolute.** Search returns `"6 years ago"`, never an ISO date.
  `publishedTimeApprox` is that string resolved against the scrape time, so it is exact to
  YouTube's own granularity and no better. It is named `Approx` on purpose. An exact timestamp
  needs a second request per video, which this actor deliberately does not make.
- **Live rows have no duration and no publish date.** That is YouTube, not a bug — a live row
  carries `liveViewerCount` instead. Verified: 20 of 20 rows under the `live` filter.
- **Shorts rows are thin by nature.** YouTube gives a Shorts result a title, a view count and a
  thumbnail. This actor adds `channelId` (100% of 51 rows). It cannot give you the channel *name* or
  *@handle* for a Short, because they are not in the payload.
- **`videoCountText` / `videoCount` on channel rows measured 0/80.** YouTube currently puts the
  subscriber count in that slot instead. The parser reads those fields by content rather than by
  name, so they will populate again if YouTube restores them — today they are empty, and this
  README says so rather than pretending.
- **No per-video enrichment.** Like count, comment count, subscriber count on a video row, full
  description, keywords and tags are **not** in the search response. They need a separate
  `/player` or `/next` call per video. This actor is one request per page, on purpose — it does not
  claim fields it cannot see.
- **Sustained very-high volume is unproven.** 86 consecutive requests came back clean. Nobody has
  run this at tens of thousands of requests an hour. If you do and YouTube starts refusing, the run
  **exits cleanly with a "blocked" status message** — never as "no results found", and never as a
  FAILED run. Switch `proxyConfiguration` to `RESIDENTIAL`, or bring your own proxy.
- **Residential proxy costs bandwidth.** A result is roughly 36 KB of response. On Apify Proxy
  `auto` (the default) bandwidth is not billed. On residential at ~$8/GB it is about $0.29 per
  1,000 rows — still comfortably inside the price, but do not switch it on out of habit.

***

### ⚖️ Legal & fair use

This actor reads YouTube's public search results — the same rows any visitor sees, with no login,
no account, no cookies and no CAPTCHA solving. It collects **no personal data**: channels are
public business/creator profiles, and no viewer or commenter information is touched.

`https://www.youtube.com/robots.txt` lists, under `User-agent: *`:

```
Disallow: /results
Disallow: /youtubei/
```

That is quoted here verbatim so you can make your own call. robots.txt is a crawling convention,
not law, and it applies to indexing crawlers; you are responsible for your own use of the output
and for compliance with YouTube's Terms of Service in your jurisdiction. Keep request volume
reasonable, do not republish YouTube content wholesale, and use the data for research, analysis
and internal tooling.

***

### ❓ FAQ

**How many results can I get for one keyword?**
Measured: 473 for `"plumber"`, 347 for `"best crm software"`, then YouTube stops paginating. Use
more keywords and more filter variants (each variant returns a genuinely different slice).

**Do I pay for duplicate rows?**
No. Duplicates within a query are dropped before they are pushed or charged. Set
`deduplicateAcrossQueries: true` if you also want a result de-duplicated across different keywords.

**Can I get the exact upload date?**
Not from search — YouTube only exposes a relative string there. `publishedTimeApprox` resolves it
against the scrape time and is labelled approximate for that reason.

**Can I get like counts, comment counts, or subscriber counts on a video row?**
No, and no honest YouTube *search* scraper can do it in one request — they are not in the search
response. Pair this with a video-detail actor if you need them.

**Does it get YouTube Shorts?**
Yes. `resultType: shorts` returns only Shorts; `resultType: any` mixes them in as youtube.com does.
Each Short comes with its `videoId`, view count, thumbnails and the uploader's `channelId`.

**Can I search a specific country or language?**
Yes — `region` (`gl`) and `language` (`hl`). YouTube ranks per country, so `region: "DE"` gives you
the German result set for the same keyword.

**Can I sort by upload date or rating?**
Yes. `sortBy: "uploadDate"` and `sortBy: "rating"` are encoded straight into YouTube's search
protobuf. So is `uploadDate: "lastHour"` — verified live returning videos 21 minutes old.

**Do I need a proxy?**
The default (Apify Proxy `auto`) is enough and was verified at 100% success across 86 consecutive
requests. Residential is there as an opt-in for very large jobs.

**Do I need a YouTube API key or an account?**
No. No key, no login, no cookies, no quota.

**What happens if YouTube blocks the run?**
It exits cleanly with an explicit "blocked" status message telling you how many requests were
refused, and suggests residential proxies. It never reports a block as "no results found", and it
never ends FAILED for a transport problem.

**What happens if YouTube changes its response shape?**
If results come back but nothing can be parsed, the run **fails loudly** rather than quietly
handing you an empty dataset. If a filter enum is renumbered, the actor detects it against
YouTube's own filter chips and corrects itself mid-run.

**Is the result order YouTube's real ranking?**
Yes. `searchPosition` preserves the order YouTube returned, per query, after de-duplication.

**What is the cheapest way to run this?**
Leave `resultType` on `video` (so you are not billed for the Shorts shelf), keep
`proxyConfiguration` on `auto`, and set `maxResultsPerQuery` to what you actually need.

***

### 🔬 How this was verified

Nothing in this README is estimated. Every number came from a run you can reproduce:

- The parser was validated **offline first** against captured live responses — 44 rows on page 1, all
  five result shapes, the live-row absence trap, and 24/24 filter encodings against YouTube's chips.
- Then on the Apify platform, build `0.1.2`: 12 runs, all SUCCEEDED, covering every result type,
  every filter axis, the depth ceiling, the health-test input and empty input.
- Fill percentages are computed over the **600-row proof run**, not a sample.

# Actor input Schema

## `searchQueries` (type: `array`):

The keywords to search YouTube for — one per line, exactly what you would type into YouTube's own search box. A single query runs dry at roughly 200-340 unique results (that is YouTube's ceiling, not ours), so volume comes from running MANY queries, not one deep one. Leave empty and the actor runs the sample query "web scraping" so a blank run still returns data.

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

YouTube's own sort order. `relevance` is the default ranking you see on youtube.com. `uploadDate` and `rating` are not exposed by any other Store actor — they are encoded straight into YouTube's search protobuf.

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

Only return results uploaded inside this window. `lastHour` is a real YouTube filter that competing actors do not expose — verified live returning videos 26-56 minutes old.

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

Which kind of result YouTube should return. These are server-side filters, verified live: `video` returned 64 video rows and zero Shorts over 3 pages, `channel` returned 20 channel rows and zero videos. `any` reproduces youtube.com exactly, which means the Shorts shelf is mixed in and you pay for those rows too — over the same 3 pages `any` returned 40 videos + 70 Shorts + 1 channel. Default is `video` so you are not billed for Shorts you did not ask for.

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

Server-side length filter. Verified live: `under3min` returned 0:45-1:52, `from3to20min` returned 6:39-19:39, `over20min` returned 55:11-11:55:00.

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

YouTube's feature checkboxes. Pick any combination — they are encoded together into one request, so the full cross-product with the sort/date/type/duration filters is reachable. Note: `live` results legitimately have no duration and no publish date.

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

Stop each query after this many unique results. YouTube's own continuation chain runs dry at roughly 200-340 unique results per query (measured: 206 for "plumber", 337 for "best crm software"), so asking for much more than that just ends the query early — it does not under-deliver silently, the log says "no further pages".

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

Hard stop across every query combined. 0 = no overall limit (each query is still bounded by "Max results per query").

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

YouTube interface language code sent as `hl`. Changes the language of relative dates ("6 years ago") and badge labels, and nudges which results rank.

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

Two-letter country code sent as `gl`. YouTube ranks search results per country, so this is how you scrape the German or Brazilian result set for the same keyword.

## `deduplicateAcrossQueries` (type: `boolean`):

Duplicates INSIDE one query are always dropped before billing (YouTube's continuation chain repeats 8-14% of rows). This switch also drops a result that already appeared under a DIFFERENT query. Leave it off for rank tracking, where the same video ranking for two keywords is two real data points; switch it on when you just want a unique list of videos.

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

Apify Proxy on `auto` is enough — verified HTTP 200 through auto and through residential on the same day. Switch to RESIDENTIAL only if a very large job starts seeing 403/429 in the log; residential bandwidth is billed and each result is ~36 KB.

## Actor input object example

```json
{
  "searchQueries": [
    "plumber"
  ],
  "sortBy": "relevance",
  "uploadDate": "any",
  "resultType": "video",
  "duration": "any",
  "features": [],
  "maxResultsPerQuery": 50,
  "maxResults": 0,
  "language": "en",
  "region": "US",
  "deduplicateAcrossQueries": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

The dataset of scraped YouTube search results (one item per result, in YouTube's own ranking order).

# 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 = {
    "searchQueries": [
        "plumber"
    ],
    "maxResultsPerQuery": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/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 = {
    "searchQueries": ["plumber"],
    "maxResultsPerQuery": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/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 '{
  "searchQueries": [
    "plumber"
  ],
  "maxResultsPerQuery": 50
}' |
apify call scrapersdelight/youtube-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/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/6lZpIX03MfP3nK6IO/builds/fS2Vvf3kcPEXiRWiO/openapi.json
