# YouTube Scraper Pro (`coregent/youtube-scraper-pro`) Actor

Full‑fidelity YouTube data extractor for videos, Shorts, live streams, and channels. Built on Apify + Puppeteer for reliable, scalable web scraping that returns complete metadata, engagement stats, caption tracks for transcripts, hashtags/keywords, description links, and channel insights — fast.

- **URL**: https://apify.com/coregent/youtube-scraper-pro.md
- **Developed by:** [Delowar Munna](https://apify.com/coregent) (community)
- **Categories:** Developer tools, SEO tools, Social media
- **Features:** Limited permissions
- **Stats:** 54 total users, 4 monthly users, 100.0% runs succeeded, 4 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 video 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/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 Scraper Pro (Apify Actor)

**Analytics-ready YouTube data** for **videos, Shorts, live streams, and channels**. Built on the **official YouTube Data API v3** with HTTP + Innertube fallbacks — **no cookies, no login, no residential proxy required** — returning engagement metrics, SEO signals, parsed description links, channel intelligence, and result provenance.

![YouTube Scraper Pro banner](https://raw.githubusercontent.com/coregentdevspace/youtube-scraper-assets/main/thumbnail-youtube-scraper-pro-01.png?v=2 "YouTube Scraper Pro — Apify Actor for complete video & channel metadata")

> **Transcripts:** this actor can attach captions per video — enable `includeTranscript` to fill a `subtitles` array (SRT, VTT, plain text, or JSON — one or more). For **standalone / bulk** transcript jobs, our dedicated **[YouTube Transcript Scraper](https://apify.com/coregent/youtube-transcript-scraper)** is purpose-built.

***

### ⚡ Need a real-time API?

This Actor also powers the **[YouTube Scraper API](https://rapidapi.com/coregentdevspace/api/youtube-scraper14)** on RapidAPI. `POST /youtube/videos` returns YouTube search results with full video metadata as JSON in a single HTTP request, with a free Basic plan and no Apify account needed.

Use the API for on-demand lookups inside your app. Use this Actor for bulk runs, schedules, datasets and integrations.

### At a Glance

- **Official-API sourced**: YouTube Data API v3 for core fields, with HTTP + Innertube fallbacks — no cookies, no logged-in session, no residential proxy
- **Scrape by** keywords, channel handles (e.g. `@mkbhd`), channel IDs, playlists, or direct video URLs
- **Returns**: title, description, duration, publish date, views, likes, comment count, category, media flags (HD/4K/HDR), hashtags, keywords, parsed description links, monetization status, and rich **channel** data
- **Provenance on every row**: which query produced it, its order, and when it was collected — unique to this actor
- **Spreadsheet-friendly**: flat camelCase keys plus a nested `channel` object; import straight into Google Sheets, Excel, or a database
- **Spend control**: cap the whole run with `maxItems`

> **Best for**: content research, SEO, competitive intelligence, trend analysis, brand monitoring, influencer discovery, and analytics-ready ETL feeds.

***

### How it works

Each video is resolved through a tiered pipeline; every tier only handles what the previous one could not, so most videos never touch a browser:

| Tier | Role | Cost |
|------|------|------|
| **HTTP extraction** | Fetches raw HTML + parses embedded JSON (`ytInitialData`, `ytInitialPlayerResponse`) | ~1s/video, no browser |
| **YouTube Data API v3** | Batch core fields (views, likes, duration, category, keywords) | 1 quota unit per 50 videos |
| **Innertube** | `/youtubei/v1/player` fallback when HTTP is soft-blocked | no browser |
| **TikHub** | Hosted-API fallback for videos the above could not resolve (e.g. datacenter-IP blocks) | paid, only when needed |
| **Puppeteer (browser)** | Last-resort DOM extraction; skipped entirely when nothing is left | rare |

**No residential proxy is required** for normal use. The actor works reliably from datacenter IPs; the TikHub tier absorbs the cases where a raw HTTP fetch is blocked.

***

### Key Features

- **API-first discovery (fast)**: keyword & **#hashtag** search via TikHub; channel / playlist / seed-video expansion via the YouTube Data API v3 — **no browser for discovery**. A browser is only a rare last-resort for videos no API tier can resolve.
- **One discovery field**: keywords, #hashtags, channels (URL / `@handle` / `UC…`), playlists (URL / `PL…`), or a **seed video** (expands to its owning channel). Plus a separate direct-video field.
- **Content coverage**: standard videos, **Shorts** (duration-detected), live/live-replay — filter with `contentType`
- **Filters & sorting**: `contentType`, `videoDuration`, `titleFilter`, `uploadDate`/date range, and `sortBy` (relevance / newest / most-viewed), applied to discovered results
- **Localization**: 37 countries (`regionCode`) and 20 languages (`language`)
- **SEO signals**: hashtags, keywords/tags, parsed outbound description links
- **Channel intelligence**: id, name, handle, URL, numeric subscriber count, totals, country, profile image, join date, description
- **Monetization detection**: `isMonetized` (+ `commentsAllowed`, `isRatingsAllowed`) when reachable
- **Detected language**: `detectedLanguage` (API hint + trigram detection)
- **Optional subtitles**: `includeTranscript` fills a `subtitles` array (SRT/VTT/txt/JSON, one or more, each + a key-value-store file link) via TikHub captions (off by default; billed only when delivered)
- **Result provenance**: `source`, `order`, `collectedAt` on every record
- **Spend cap**: `maxItems` bounds the total results saved across the whole run

***

### Quick Start (Apify)

1. **Create an Actor task** and paste one of the JSON inputs below.
2. Set `maxItems` if you want to cap total spend for the run.
3. Run. Export JSON/CSV/Excel to your datastore, Google Sheets, or S3.

***

### Input Schema

The input form is organized into four collapsible sections:

- **1. What to scrape** — the combined discovery field, direct video URLs/IDs, and result limits
- **2. Filters & Sorting** — content type, duration, title filter, sort, dates, trending
- **3. Advanced** — localization and output format
- **4. Transcript** — optional caption extraction

#### Input Parameters

| # | Field | Key | Type | Required | Default | Description |
|---|-------|-----|------|----------|---------|-------------|
| 1 | Search, Channels, Playlists & Discovery | `searchQueries` | Array<string> | No | `[]` | Discovery sources, auto-detected: a **keyword**, a **#hashtag**, a **channel** (URL / `@handle` / `/channel/UC…` / `/c/…` / `/user/…` / bare `UC…`), a **playlist** (URL or bare `PL…`/`UU…`), or a **video** (URL/ID — treated as a **seed**: its owning channel is scraped). Each source capped by `maxResultsPerQuery` |
| 2 | YouTube URLs or IDs | `startUrls` | Array<string> | No | `[]` | Direct videos — one record per entry, for that video only (no expansion, exempt from filters). Full URLs (`watch?v=…`, `youtu.be/…`, `/shorts/…`) or bare 11-char IDs. Non-video entries are skipped |
| 3 | Max videos per source | `maxResultsPerQuery` | integer | No | `10` | Caps each discovery source (keyword/hashtag/channel/playlist/seed) individually. Min 1. Doesn't apply to YouTube URLs/IDs (one video each) |
| 4 | Max results (whole run) | `maxItems` | integer | No | `0` | **Global spend cap** — total results saved across the run, applied after filter + sort. `0` = no global limit |
| 5 | Content type | `contentType` | string | No | `"all"` | `all` / `videos` / `shorts` / `live`. Shorts detected by duration (≤ 60s); live from broadcast status. Discovered results only |
| 6 | Duration | `videoDuration` | string | No | `"all"` | `all` / `short` (<4m) / `medium` (4–20m) / `long` (>20m). Discovered results only |
| 7 | Sort results by | `sortBy` | string | No | `"relevance"` | `relevance` (discovery order) / `date` (newest) / `views` (most viewed). Sorts the discovered set before the cap |
| 8 | Upload date filter | `uploadDate` | string | No | `"any"` | `any` / `hour` / `today` / `week` / `month` / `year`. Applies to all discovered sources |
| 9 | Title filter | `titleFilter` | string | No | `""` | Comma-separated keywords; keep results whose title contains at least one (OR, case-insensitive) |
| 10 | Include trending videos | `getTrending` | boolean | No | `false` | Also add the Country's trending videos as a source (official Data API; requires an API key) |
| 11 | From Date | `dateFrom` | string | No | `""` | Publish-date lower bound (YYYY-MM-DD). Applies to all discovered sources |
| 12 | To Date | `dateTo` | string | No | `""` | Publish-date upper bound (YYYY-MM-DD). Applies to all discovered sources |
| 13 | Country | `regionCode` | string | No | `"US"` | ISO-3166-1 alpha-2. 37 options (US, GB, DE, IN, JP, KR, ID, BR, NG, …) |
| 14 | Language | `language` | string | No | `"en"` | IETF BCP-47. 20 options (en, es, de, ja, ko, hi, id, ar, …) |
| 15 | Output format | `outputMode` | string | No | `"rich"` | `rich` (nested JSON) or `analyticsFlat` (nested objects flattened into `channel_name`, `features_isHD`, … and arrays joined — spreadsheet-friendly) |
| 16 | Include Transcript | `includeTranscript` | boolean | No | `false` | Fill the `subtitles` array via TikHub captions. Adds ~1–2s/video; ~60–80% of videos have captions. Billed per **delivered** subtitle |
| 17 | Subtitle formats | `subtitleFormats` | Array<string> | No | `["srt"]` | Multi-select: any of `srt`, `vtt` (timestamped), `txt` (plain text), `json` (`{start,dur,text}` segments). Each selected format is added to the `subtitles` entry |
| 18 | Transcript Language | `transcriptLanguage` | string | No | `""` | Preferred caption language; empty = auto (first available). Only used when Include Transcript is on |

> **Filters apply to discovered results only** — videos listed directly in *YouTube URLs or IDs* are always returned as-is (exempt from `contentType`/`videoDuration`/`titleFilter`/date).

> **Keyword & hashtag search requires a TikHub key**; channel/playlist/seed expansion and metadata require the YouTube API key. Both are set as Apify secrets.

> Tip: start with a small `maxResultsPerQuery` (and/or a `maxItems`) to validate your setup, then scale up.

#### Input Example — 01: Search with date filtering and a spend cap

```json
{
  "searchQueries": ["AI tools", "machine learning", "#openai"],
  "maxResultsPerQuery": 20,
  "maxItems": 50,
  "contentType": "videos",
  "sortBy": "views",
  "uploadDate": "month",
  "videoDuration": "medium",
  "regionCode": "US",
  "language": "en"
}
```

#### Input Example — 02: Direct video URLs and IDs

```json
{
  "startUrls": [
    "https://www.youtube.com/watch?v=7Sx0o-41r2k",
    "https://youtu.be/5oAnKSCP4do",
    "QJBP2uy8LcU"
  ]
}
```

#### Input Example — 03: Channels, playlists, and a seed video (all in Discovery)

```json
{
  "searchQueries": [
    "@MKBHD",
    "UCXuqSBlHAE6Xw-yeJA0Tunw",
    "https://www.youtube.com/playlist?list=PLBCF2DAC6FFB574DE",
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "contentType": "all",
  "maxResultsPerQuery": 25,
  "regionCode": "US",
  "language": "en"
}
```

> The last entry is a **video** URL — in the Discovery field it's a **seed**: the channel that owns it is scraped for up to `maxResultsPerQuery` videos. The **same** value in the *YouTube URLs or IDs* field would instead return only that one video.

#### Input Example — 04: With subtitles

```json
{
  "searchQueries": ["langchain tutorial"],
  "maxResultsPerQuery": 10,
  "includeTranscript": true,
  "subtitleFormats": ["srt", "json"],
  "transcriptLanguage": "en"
}
```

***

### What you pay

This actor uses Apify's **pay-per-event** pricing. The live rates are shown on the **Pricing** tab of the actor page — this README deliberately does not quote figures, because the amounts are set on the platform and can change.

You are charged for:

| Event | Fires when | Notes |
|-------|-----------|-------|
| **Result** | A video record is saved to the dataset | The main charge. One per video returned. |
| **Discovery (search)** | A discovery source (keyword, hashtag, channel, playlist, or seed video) **returns at least one result** | **A discovery that returns zero results is not charged.** **Direct video URLs never incur a discovery charge** — you pay only for the result. |
| **Transcript** | A transcript is actually delivered for a video | Only when `includeTranscript` is on **and** captions exist. Videos without captions are **not** charged. |

**Controlling spend:**

- Set **`maxItems`** to cap the total number of results (and therefore the total result charges) for the whole run.
- Use **`maxResultsPerQuery`** to cap each individual source.
- Leave **`includeTranscript`** off unless you need spoken text — it adds a per-delivered-transcript charge.
- Error/unavailable rows (see *Errors & limitations*) are not billed as normal results.

For worked cost examples at the current rates, see the **Pricing** tab on the actor page.

***

### Output Schema

Each record has **35 top-level fields**. Legacy keys (`VideoId`, `PageURL`, `publishDate`) are retained alongside their normalized aliases (`id`, `url`, `publishedAt`) for backward compatibility. Set `outputMode: "analyticsFlat"` to flatten nested objects into top-level columns for spreadsheets.

#### Core fields

| # | Field | Type | Description |
|---|-------|------|-------------|
| 1 | type | String | One of `video`, `shorts`, `live`, `stream` |
| 2 | isShort | Boolean | Convenience mirror of `type === "shorts"` |
| 3 | VideoId | String | YouTube video ID (legacy key) |
| 4 | id | String | YouTube video ID (camelCase alias of `VideoId`) |
| 5 | PageURL | String | Canonical watch URL (legacy key) |
| 6 | url | String | Canonical watch URL (camelCase alias of `PageURL`) |
| 7 | title | String | Video title |
| 8 | thumbnailUrl | String | null | Primary/hero thumbnail URL |
| 9 | publishDate | String | null | Publish date (date, or ISO if a time is known) |
| 10 | publishedAt | String (ISO-8601) | null | Full timestamp **when a real time is known**; `null` when only a date is available (never fabricated) |
| 11 | duration | String | null | `HH:MM:SS` (e.g. `00:22:43`) |
| 12 | durationSeconds | Integer | null | Duration in seconds |
| 13 | viewCount | Integer | null | Total views |
| 14 | likeCount | Integer | null | Total likes |
| 15 | commentCount | Integer | null | Public comment count |
| 16 | category | String | null | Video category |

#### State flags

| # | Field | Type | Description |
|---|-------|------|-------------|
| 17 | isLive | Boolean | Whether the item is/was a live stream |
| 18 | isMembersOnly | Boolean | Members-only gated flag |
| 19 | isPrivate | Boolean | Private/unavailable to public |
| 20 | isFamilySafe | Boolean | null | Family-safe flag if exposed |
| 21 | isMonetized | Boolean | null | Monetization detectable flag |
| 22 | isRatingsAllowed | Boolean | null | Whether likes/dislikes are enabled |
| 23 | commentsAllowed | Boolean | null | Whether comments are enabled |

#### Content & media

| # | Field | Type | Description |
|---|-------|------|-------------|
| 24 | description | String | null | Creator-written description |
| 25 | descriptionLinks | Array<Object> | Outbound links parsed from the description (`{url, text}`) |
| 26 | subtitles | Array<Object> | Captions — empty `[]` unless `includeTranscript` is on and captions exist. See shape below |
| 27 | keywords | Array<String> | Tags/keywords |
| 28 | hashtags | Array<String> | Hashtags from title/description |
| 29 | features | Object | Media flags: `isHD`, `is4K`, `isHDR`, `isVR180`, `is360` (each Boolean | null) |

**`subtitles` entry:** one object per caption track, carrying `type`, `language`, and — for **each** selected `subtitleFormats` — an inline `<fmt>` value plus a `<fmt>Url` link to the file in the run's key-value store (signed URL on the platform). `<fmt>` is `srt`/`vtt`/`txt` (String) or `json` (Array of `{start, dur, text}` in seconds). `type` is `auto_generated` (the TikHub captions list does not distinguish auto vs manual, so this is best-effort). Example with `subtitleFormats: ["srt","json"]`:

```json
"subtitles": [
  {
    "type": "auto_generated", "language": "en",
    "srtUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/subtitles_<vid>_en_auto_generated.srt?signature=…",
    "srt": "1\n00:00:01,360 --> 00:00:03,040\n[♪♪♪]\n\n2\n…",
    "jsonUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/subtitles_<vid>_en_auto_generated.json?signature=…",
    "json": [ { "start": 1.36, "dur": 1.68, "text": "[♪♪♪]" }, … ]
  }
]
```

#### Derived analytics

Computed by this actor from the fields above — **not** source-native YouTube values. `null` when the required inputs are missing.

| # | Field | Type | Description |
|---|-------|------|-------------|
| 30 | engagementRate | Number | null | `(likeCount + commentCount) / viewCount` |
| 31 | videoAgeDays | Integer | null | Whole days since publication |
| 32 | viewsPerDay | Integer | null | `viewCount / max(videoAgeDays, 1)` |
| 33 | detectedLanguage | String | null | 2-letter language code — API audio/default-language hint, else trigram detection over title+description |

#### Channel & provenance

| # | Field | Type | Description |
|---|-------|------|-------------|
| 34 | channel | Object | Channel metadata (see below) |
| 35 | provenance | Object | `order` (Integer | null), `source` (`keyword`/`hashtag`/`channel`/`playlist`/`seed`/`trending`/`startUrls`), `query`, `collectedAt` (ISO-8601) |

**`channel` object:** `id`, `name`, `handle`, `url`, `subscriberCount` (**Integer** | null — normalized), `subscriberCountText` (String | null — the display form `"4.42M subscribers"` when the video page is read; the raw count as a string, e.g. `"201000"`, on the API-only path), `totalViews`, `totalVideos`, `country`, `profileImage`, `description`, `createdAt`, `links`.

Channel-level fields (`subscriberCount`, `totalViews`, `totalVideos`, `country`, `description`, `profileImage`, `handle`, `url`, `createdAt`) are populated by a batched `channels.list` call (one per unique channel per run) **when a YouTube API key is configured**; without a key most remain `null`. `links` is not populated (it requires scraping the channel About page) and is always empty.

#### Sample output

A real record from a keyword run (`searchQueries: ["tesla"]`, `includeTranscript: true`, `subtitleFormats: ["srt","vtt","txt","json"]`). Long fields (`srt`/`vtt`/`txt`/`json`, description) are truncated here with `…`; the caption files are also linked in the run's key-value store. On datacenter IPs the DOM-only fields (`isMonetized`/`commentsAllowed`/`isRatingsAllowed`) may be `null` — see *Field availability* below.

```json
{
  "type": "video",
  "isShort": false,
  "VideoId": "qoh_rrgcUQ4",
  "id": "qoh_rrgcUQ4",
  "PageURL": "https://www.youtube.com/watch?v=qoh_rrgcUQ4",
  "url": "https://www.youtube.com/watch?v=qoh_rrgcUQ4",
  "title": "Tesla's Most Underrated Achievement Just Became Normal",
  "thumbnailUrl": "https://i.ytimg.com/vi/qoh_rrgcUQ4/maxresdefault.jpg",
  "publishDate": "2026-07-27",
  "publishedAt": "2026-07-27T18:51:11Z",
  "duration": "00:40:47",
  "durationSeconds": 2447,
  "viewCount": 15390,
  "likeCount": 985,
  "commentCount": 71,
  "category": "Science & Technology",
  "isLive": false,
  "isMembersOnly": false,
  "isPrivate": false,
  "isFamilySafe": null,
  "isMonetized": null,
  "isRatingsAllowed": null,
  "commentsAllowed": null,
  "description": null,
  "descriptionLinks": [],
  "subtitles": [
    {
      "type": "auto_generated",
      "language": "en",
      "srtUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/subtitles_qoh_rrgcUQ4_en_auto_generated.srt?signature=…",
      "srt": "1\n00:00:00,080 --> 00:00:04,000\nTeslas are now driving coast to coast\n…",
      "vttUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/subtitles_qoh_rrgcUQ4_en_auto_generated.vtt?signature=…",
      "vtt": "WEBVTT\n\n00:00:00.080 --> 00:00:04.000\nTeslas are now driving coast to coast\n…",
      "txtUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/subtitles_qoh_rrgcUQ4_en_auto_generated.txt?signature=…",
      "txt": "Teslas are now driving coast to coast without a single intervention. …",
      "jsonUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/subtitles_qoh_rrgcUQ4_en_auto_generated.json?signature=…",
      "json": [ { "start": 0.08, "dur": 3.92, "text": "Teslas are now driving coast to coast" }, "…" ]
    }
  ],
  "keywords": ["tesla", "tsla", "tesla stock", "investing", "…"],
  "hashtags": [],
  "features": { "isHD": null, "is4K": null, "isHDR": null, "isVR180": null, "is360": null },
  "engagementRate": 0.06862,
  "videoAgeDays": 0,
  "viewsPerDay": 15390,
  "detectedLanguage": "en",
  "channel": {
    "id": "UC4DBLlq1x0AKmip1QJUcbXg",
    "name": "Brighter with Herbert",
    "subscriberCount": 201000,
    "subscriberCountText": "201000",
    "totalViews": 89815604,
    "totalVideos": 2516,
    "country": "US",
    "description": "Interviews about the future (Tesla, SpaceX, Robots and AI) …",
    "profileImage": "https://yt3.ggpht.com/…",
    "createdAt": "2012-10-24",
    "handle": "@brighterwithherbert",
    "url": "https://www.youtube.com/@brighterwithherbert"
  },
  "provenance": { "source": "keyword", "query": "tesla", "order": 1, "collectedAt": "2026-07-28T00:26:25.553Z" }
}
```

***

### Field availability & nullability

This actor extracts **public** data. Not every field is available for every video — a `null` means "not available for this video", not an error. Plan your downstream models for nullable fields.

| Field(s) | May be `null` when |
|----------|--------------------|
| `likeCount`, `commentCount` | The creator has hidden likes/comments, or comments are disabled |
| `isMonetized`, `isFamilySafe`, `isRatingsAllowed` | The signal is not exposed for that video |
| `publishedAt` | Only a publish **date** (no time) is available — the field stays `null` rather than inventing a time |
| `category` | Not exposed in the available metadata |
| `channel.subscriberCount` / `subscriberCountText` | The channel hides its subscriber count |
| `channel.totalViews`, `totalVideos`, `country`, `description`, `profileImage`, `handle`, `url`, `createdAt` | No API key configured (these come from a `channels.list` enrichment call), or the channel hides them |
| `channel.links` | Always empty — requires scraping the channel About page (not part of this actor) |
| `subtitles` | Empty `[]` unless `includeTranscript` is on **and** the video has captions (~60–80% do) |
| `detectedLanguage` | Title/description too short to detect and no API language hint |
| **Dislikes** | **Always unavailable** — YouTube no longer exposes dislike counts publicly |

> Captions are opt-in via `includeTranscript` (→ `subtitles`). For standalone/bulk transcript jobs, use the [YouTube Transcript Scraper](https://apify.com/coregent/youtube-transcript-scraper).

***

### Errors & limitations

- **Unavailable videos** (private, removed, age-restricted, geo-blocked, members-only) produce a typed **error record** rather than vanishing silently (see below).
- **Date filtering** applies to **Search Keywords only**, not direct URLs.
- **Dislikes** are never available (YouTube removed public dislike counts).
- **Transcripts/captions** are opt-in (`includeTranscript` → `subtitles` array); for standalone/bulk transcript jobs use the dedicated transcript actor linked above.
- **Rate limits**: the actor throttles itself to respect YouTube; very large jobs should be batched by topic/channel.

#### Error records

When an input can't be resolved, the dataset gets a typed error row instead of dropping it silently. Error rows are **never billed as results** and don't count against `maxItems`. Filter them out with `type != "error"`.

```json
{
  "type": "error",
  "input": "https://www.youtube.com/watch?v=...",
  "videoId": "...",
  "errorCode": "PRIVATE",
  "errorMessage": "This video is private",
  "retryable": false,
  "source": "startUrls",
  "collectedAt": "2026-07-25T10:00:00.000Z"
}
```

`errorCode` is one of `PRIVATE`, `AGE_RESTRICTED`, `MEMBERS_ONLY`, `VIDEO_UNAVAILABLE`, `GEO_BLOCKED`, `BLOCKED`, `NETWORK_ERROR`, `EXTRACTION_FAILED`. `retryable` tells you whether re-running the input might succeed (transient) or never will (permanent).

#### Run summary

Every run writes a **`RUN_SUMMARY`** record to the default key-value store (and logs it): inputs provided, results saved, and a per-code error breakdown — so you can tell "requested vs returned" at a glance without scanning the dataset.

***

### Integrations

Because this runs on the Apify platform, its dataset plugs into the standard Apify integrations with no extra work:

- **Export**: JSON, CSV, Excel, XML directly from the dataset
- **Google Sheets / Airtable / databases**: push results via Apify integrations
- **Automation**: Make, Zapier, and n8n connectors
- **Webhooks**: trigger downstream workflows on run completion
- **API & clients**: fetch results via the Apify API, or the JavaScript / Python Apify clients
- **MCP / AI agents**: the dataset is reachable by AI-agent tooling through the Apify platform

***

### Compared to alternatives

Honest positioning against other YouTube actors on Apify:

| | YouTube Scraper Pro (this actor) | Typical high-volume scrapers | Typical low-cost scrapers |
|---|---|---|---|
| Data source | **Official YouTube Data API v3** + HTTP/Innertube/TikHub | Unofficial extraction | Unofficial extraction |
| Cookies / login required | **No** | Sometimes | Sometimes |
| Result provenance (source/order/time) | **Yes** | Rarely | Rarely |
| Monetization + parsed description links | **Yes** | Sometimes | Rarely |
| Single-video URLs | **Supported** | Varies | Often not allowed |
| Transcripts | Separate [transcript actor](https://apify.com/coregent/youtube-transcript-scraper) | Sometimes built in | Rarely |
| Raw throughput (videos/sec) | Competitive, not the leader | **Higher on some actors** | Varies |
| Price | Mid-market | Often lower | **Lowest** |

**Where we win:** official-API sourcing, provenance, monetization/description-link depth, single-video support, and a simple schema. **Where others win:** raw throughput and rock-bottom price. Choose this actor when you want trustworthy, analytics-ready data over the cheapest-possible bulk dump.

***

### Performance

- **HTTP-first**: most videos are saved directly over HTTP without launching a browser; the Puppeteer tier is a rare last resort.
- **Concurrency**: up to 10 parallel requests.
- **Memory**: default 2 GB (min 1 GB / max 4 GB). Pure-HTTP runs work at 1 GB; the default leaves headroom for the occasional browser fallback.

> ⏳ **Throughput benchmark pending re-measurement.** The previously published figure (~7s/video) described an older browser-heavy path and no longer reflects the HTTP-first architecture. A refreshed videos-per-minute benchmark will be added after a fresh Apify run.

***

### Popular Use Cases

- **SEO research**: surface keywords, tags, hashtags, and linking practices
- **Content strategy**: analyze formats, titles, and posting cadence
- **Competitive intelligence**: benchmark creators and track launches
- **Market/academic research**: study trends by niche, region, or language
- **Brand monitoring**: find mentions and categorize sentiment downstream
- **Influencer discovery**: filter by views/engagement within your topics
- **Analytics ETL**: feed provenance-tagged, normalized rows into a warehouse

***

### FAQ

**Where does the data come from?**
The official YouTube Data API v3 for core fields, with HTTP page extraction and the Innertube API as fallbacks, and a hosted-API tier (TikHub) for videos that datacenter IPs can't reach directly. No cookies, no login, no residential proxy.

**Do I need a residential proxy?**
No. The actor works reliably from datacenter IPs. Residential proxy is not required and not part of the standard input.

**How do I control how much I spend?**
Set `maxItems` to cap the total results for the whole run, and `maxResultsPerQuery` to cap each source. Pricing is per event — see *What you pay* and the Pricing tab.

**What am I charged for?**
Per result saved, plus a discovery event for search/channel/playlist sources. Direct video URLs incur no discovery charge. See *What you pay*.

**Can I get transcripts or subtitles?**
Yes — set `includeTranscript: true` to fill the `subtitles` array via TikHub captions (off by default; billed per delivered subtitle). Choose one or more `subtitleFormats` — `srt`/`vtt` (timestamped), `txt` (plain text), or `json` (`{start,dur,text}` segments); each entry includes every selected format inline plus a key-value-store file link. For **standalone / bulk** transcript jobs, the dedicated [YouTube Transcript Scraper](https://apify.com/coregent/youtube-transcript-scraper) is purpose-built.

**Can I scrape a whole channel?**
Yes — put the channel's `@handle`, channel ID (`UC…`), or channel URL into the **Discovery** field. You can also drop in a **video URL/ID** there and it's treated as a *seed* — resolved to the channel that owns it. Use `maxResultsPerQuery` to limit videos per source.

**Can I search by hashtag?**
Yes — put a `#tag` (or a `youtube.com/hashtag/<tag>` URL) in the Discovery field. It's routed through TikHub search (requires a TikHub key).

**Can I target a country or language?**
Yes — set `regionCode` (37 countries) and `language` (20 codes).

**Can I sort or filter results?**
Yes — `sortBy` (relevance / newest / most-viewed), plus `contentType`, `videoDuration`, `titleFilter`, `uploadDate`, and `dateFrom`/`dateTo`. These are applied to the discovered set. Videos listed directly in *YouTube URLs or IDs* are exempt (always returned).

**Can I get trending videos?**
Yes — set `getTrending: true` to also scrape the Country's currently trending videos (via the official Data API `chart=mostPopular`). Requires a YouTube API key; the count follows `maxResultsPerQuery`.

**Can I filter by date?**
Yes — `dateFrom` and `dateTo` (YYYY-MM-DD), applied to all discovered sources. (For relative windows, `uploadDate` is simpler.)

**What about Shorts and live videos?**
Use `contentType` (`all` / `videos` / `shorts` / `live`). Shorts are detected by duration (≤ 60s), live from broadcast status. Every record is tagged with `type` (`video`/`shorts`/`live`) and `isShort`/`isLive` flags. Videos listed directly in *YouTube URLs or IDs* are always returned regardless of this filter.

**Are dislikes available?**
No — YouTube no longer exposes dislike counts publicly. There is no dislike field.

**Why are some fields `null`?**
`null` means the value isn't public for that video (hidden counts, restricted content, or channel data not fetched). See *Field availability & nullability*.

**How do I import into Google Sheets or Excel?**
Export the dataset as CSV/Excel, or push it via the Apify Google Sheets integration. Flat camelCase keys make columns clean; the `channel` object expands into `channel/...` columns.

**What happens to unavailable videos?**
Private, removed, age-restricted, geo-blocked, or members-only videos produce a typed **error record** (with an `errorCode` and a `retryable` flag) instead of vanishing. Error records aren't billed as results and don't count against `maxItems`. A `RUN_SUMMARY` in the key-value store gives you the requested-vs-returned totals and a per-code error breakdown.

***

### Best Practices

1. **Start small**: validate with `maxResultsPerQuery: 5–10` (and a `maxItems` cap).
2. **Filter early**: set `contentType` and `videoDuration`; use `uploadDate`/`dateFrom`/`dateTo` to narrow results.
3. **Bulk processing**: paste many entries into **Discovery** (keywords / channels / playlists / hashtags) or **YouTube URLs or IDs** (one per line).
4. **Batch thoughtfully**: group by topic/channel.
5. **Schema-first**: build downstream models against the stable keys above; treat nullable fields as nullable.

***

### Compliance

- Intended for **legitimate research & business intelligence**.
- Collects only **public** information.
- Uses the official YouTube Data API v3; respects its Terms of Service.
- Respect YouTube's Terms of Service and applicable laws in your jurisdiction.

***

### Changelog

- **v3.1 (this release — pending deploy)**:
  - ⚡ **API-first discovery**: keyword & **#hashtag** search via TikHub; channel/playlist/**seed-video** expansion via the YouTube Data API — **no browser for discovery** (was the main slowdown on datacenter IPs). Browser is now a rare last-resort for videos no API tier can resolve.
  - 🧭 **Input reorganized** to match the sibling video-finder: one combined **Discovery** field + a **Direct URLs** field; `contentType` select and `titleFilter` replace the per-type toggles/caps; discovery filters/sort applied client-side (fetch → filter → sort → cap).
  - 🌐 **`detectedLanguage`** field (API hint + `franc-min` trigram detection)
  - 📝 **Optional `subtitles`** (`includeTranscript` + multi-select `subtitleFormats` (srt/vtt/txt/json) + `transcriptLanguage`) via TikHub captions — each entry has inline content + a key-value-store file link; off by default, billed per delivered subtitle (new `transcript` pay-per-event)
  - 🏷️ Channel enrichment now also fills `handle`, `url`, `createdAt` for API-discovered records
- **v3.0**:
  - 🔑 API keys wired as Apify secrets (`@youtube_api_key_scraper_pro`, shared `@tikhub_api_key`) — no hardcoded keys
  - 🛰️ **New TikHub reliability tier** between Innertube and the browser: recovers datacenter-blocked videos and avoids browser visits
  - 🧮 **Global spend cap** via `maxItems`
  - 🧱 **Normalized output**: `id`/`url` aliases, `publishedAt` (ISO, null when only a date is known), `isShort`, numeric `channel.subscriberCount` + `subscriberCountText`
  - 📊 **Derived analytics**: `engagementRate`, `videoAgeDays`, `viewsPerDay`
  - 🧾 **`analyticsFlat` output mode** for spreadsheet-friendly flat columns
  - 🧮 Discovery event is **not charged when a search/channel returns zero results**
  - 🧯 **Typed error records** for failed inputs (never billed) + a **`RUN_SUMMARY`** in the key-value store
  - 🔤 **Sorting** (`sortBy`) and **server-side filters** (`uploadDate`, `duration`) for search
  - 🔥 **Trending videos** input (`getTrending`) via the official Data API
  - 📡 **Live-stream control** (`includeLiveStreams`) + **per-content-type caps** (`maxVideos`/`maxShorts`/`maxLiveStreams`)
  - 🏷️ **Channel enrichment**: `channel.totalViews`/`totalVideos`/`country`/`description` filled via a cached `channels.list` call; `profileImage` now populated from the video page
  - 🗂️ Input form reorganized (What to scrape / Filters / Advanced); localization expanded to 37 countries and 20 languages
  - 🧠 Memory default 4 GB → 2 GB (browser fallback is now rare)
  - 📝 Documentation reconciled with the code (30 fields; captions/transcripts pointed to the dedicated actor)
- **v2.8**: 3-tier hybrid extraction (HTTP + API + optimized browser); graceful degradation on datacenter IPs
- **v2.7**: Removed captions field (required residential proxy, always null on datacenter IPs)
- **v2.6**: Hybrid YouTube Data API v3 + web scraping architecture; residential proxy made optional
- **v2.5**: Date-range filtering for search queries; bulk URL upload
- **v2.0**: Performance tuning, richer channel fields, improved localization

***

### Get Help

- Issues & feature requests → GitHub / Apify support
- Need something custom? Open an issue describing your dataset needs

# Actor input Schema

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

Discovery sources — each entry is auto-detected as one of: (1) a search keyword (e.g. "ai automation tutorial"); (2) a channel — URL, @handle, /channel/UC…, /c/…, /user/…, or a bare channel ID (UC…); (3) a playlist — URL (…?list=…) or a bare playlist ID (PL…, UU…); (4) a video URL or 11-char video ID — treated as a SEED (its owning channel is found and that channel's videos are returned, not just the one video); (5) a hashtag — #tag or a youtube.com/hashtag/<tag> URL. Each source is capped by "Max videos per source". To fetch a single video by itself, use the "YouTube URLs or IDs" field instead.

## `startUrls` (type: `array`):

Direct YouTube videos to scrape — one output record per entry, for that video only (no channel expansion, exempt from the Filters below). Accepts full URLs (https://www.youtube.com/watch?v=…, https://youtu.be/…, /shorts/…) or bare 11-character video IDs (e.g. dQw4w9WgXcQ). Channel/playlist/keyword entries here are skipped — put those in the Discovery field above.

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

Maximum videos to take from EACH discovery source (keyword, hashtag, channel, playlist, or seed-video channel) before filtering. Does not apply to the YouTube URLs/IDs field (one video each).

## `maxItems` (type: `integer`):

Global cap on the total number of results saved across the entire run — your spend ceiling. 0 means no global limit (each source is still capped by 'Max videos per source'). Applied after filtering and sorting.

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

Filter discovered results by content type. Shorts are detected by duration (≤ 60s); live from broadcast status. Does not affect videos listed in YouTube URLs/IDs.

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

Filter by video length: short (< 4 min), medium (4–20 min), long (> 20 min).

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

Order of results. 'Relevance' keeps discovery order (keyword search relevance / channel newest-first). 'Date' and 'Views' sort the discovered set before the total cap is applied.

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

Relative published-date filter, applied to ALL discovered sources. Combined with From/To Date below (the stricter bound wins).

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

Comma-separated keywords. Keep only results whose title contains at least one of them (OR logic, case-insensitive). Leave empty to disable.

## `getTrending` (type: `boolean`):

Also add the region's trending ("most popular") videos as a source (via the official YouTube Data API). Requires an API key. Uses the Country setting; capped by YouTube. These rows are marked provenance.source = "trending".

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

Only include videos published on or after this date (YYYY-MM-DD). Applies to ALL discovered sources. Leave empty to disable.

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

Only include videos published on or before this date (YYYY-MM-DD). Applies to ALL discovered sources. Leave empty to disable.

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

Country for localization (ISO 3166-1 alpha-2). Also selects the category list used for the category name.

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

Preferred language (IETF BCP-47). Limited effect in API mode.

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

How each record is shaped. 'Rich' keeps nested objects (channel, features, provenance). 'Analytics-flat' flattens them into top-level columns (channel_name, features_isHD, …) and joins keyword/hashtag arrays — easier for Google Sheets, Excel, and BI tools.

## `includeTranscript` (type: `boolean`):

Extract captions for each result and fill the `subtitles` array (otherwise it stays empty). Each entry has the caption text, a `type`, the `language`, and a link to the file in the run's key-value store. Adds ~1-2 seconds per video; about 60-80% of videos have captions available. Billed as a separate `transcript` pay-per-event charge, only for videos where a subtitle is actually delivered.

## `subtitleFormats` (type: `array`):

Which subtitle format(s) to include in each `subtitles` entry — pick one or more. 'SRT' and 'VTT' keep timestamps (SubRip / WebVTT); 'Plain text' is the spoken text only; 'JSON' is an array of `{ start, dur, text }` segments. All are derived from one caption fetch (no extra API cost per format). Only used when Include Transcript is enabled.

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

Preferred caption language. Auto-detect uses the first available track. Only used when Include Transcript is enabled.

## Actor input object example

```json
{
  "searchQueries": [
    "Claude"
  ],
  "startUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "maxResultsPerQuery": 10,
  "maxItems": 0,
  "contentType": "all",
  "videoDuration": "all",
  "sortBy": "relevance",
  "uploadDate": "any",
  "titleFilter": "",
  "getTrending": false,
  "dateFrom": "",
  "dateTo": "",
  "regionCode": "US",
  "language": "en",
  "outputMode": "rich",
  "includeTranscript": false,
  "subtitleFormats": [
    "srt"
  ],
  "transcriptLanguage": ""
}
```

# Actor output Schema

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

No description

# 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": [
        "Claude"
    ],
    "startUrls": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("coregent/youtube-scraper-pro").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": ["Claude"],
    "startUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
}

# Run the Actor and wait for it to finish
run = client.actor("coregent/youtube-scraper-pro").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": [
    "Claude"
  ],
  "startUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ]
}' |
apify call coregent/youtube-scraper-pro --silent --output-dataset

```

## MCP server setup

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

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/S3GnZ8NRXKo5tuRhZ/builds/5vY6EMbdyAAZM7IHO/openapi.json
