# YouTube Scraper (Videos, Comments, Transcripts) (`eccentric_layout/youtube-scraper`) Actor

Scrape YouTube without an API key: video metadata, comments, and transcripts/captions by video URL, video ID, or keyword search. Uses YouTube's InnerTube endpoints. Export JSON/CSV/Excel.

- **URL**: https://apify.com/eccentric\_layout/youtube-scraper.md
- **Developed by:** [Shahryar](https://apify.com/eccentric_layout) (community)
- **Categories:** Videos, Social media, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use 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 Scraper – Videos, Comments & Transcripts (No API Key)

**YouTube Scraper** extracts structured data from **YouTube** without an API key or login: **video metadata**, **comments** (with replies), and **transcripts/captions** — by video URL, video ID, or keyword search. Export to **JSON, CSV, or Excel**.

This is a no-API-key **YouTube comments API**, **YouTube transcript scraper**, and video-metadata scraper in one Actor. Built for **content researchers, marketers, data teams, and creators** who need YouTube data at scale without the official Data API's quota limits.

### What it does

- 🎬 **Video metadata** – title, description, channel name/ID/URL, view count, approximate like count, publish/upload date, duration, category, tags/keywords, thumbnails, and the list of available caption languages.
- 💬 **Comments** – author, text, like count, reply count, relative published time, pinned/hearted flags, and an avatar URL. Optionally expand reply threads.
- 📝 **Transcripts** – full transcript text plus timestamped segments, for manual or auto-generated captions, in your preferred language. Best-effort (see FAQ).
- 🔎 **Keyword search** – run a YouTube video search and automatically enrich every matching video.
- 🌍 **Language control** – pick the caption language and prefer human captions over auto-generated ones.
- 📤 **Export anywhere** – download the dataset as **JSON, CSV, or Excel**, or pull it via the Apify API.

### Why this scraper

- **No API key, no login, no quota.** Uses YouTube's internal **InnerTube** endpoints (the same JSON APIs the website calls), reading the live client config from the watch page so the client version is never stale.
- **Honest transcripts.** Captions are gated by YouTube; this Actor uses the most reliable path available (InnerTube `get_transcript` via the iOS client, with a timedtext fallback) and is transparent about when a transcript can't be obtained.
- **Built for reliability.** Residential proxy by default, fresh proxy session per video, retries with jitter, and resumable state so a restarted run does not refetch finished videos.
- **One Actor, four data types.** Every record carries a `type` field (`video`, `comment`, `transcript`, `searchResult`) so you can split the dataset cleanly.
- **Pairs with our other scrapers** for social/video data (TikTok, Instagram, Reddit, and more) when you need cross-platform coverage.

### How it works (and why it's reliable)

1. **Seed** – fetches the watch page once per video and extracts the live `INNERTUBE` config plus the initial comments token.
2. **Metadata** – read straight from the player response (no extra request).
3. **Transcript** – resolves the caption track list (language + manual/auto) from the mobile `player` response, then fetches the text in two stages:
   - **Primary:** the InnerTube `get_transcript` (transcript-panel) endpoint, called with the **iOS** mobile client. The `params` is a base64-protobuf built from the video ID + chosen track; this endpoint still returns segment text where the plain caption URL does not.
   - **Fallback:** the timedtext caption `baseUrl` (`fmt=json3`). The desktop/web client returns empty caption bodies (`exp=xpe` / PoToken gate), so mobile clients are used on purpose, but even mobile caption URLs can be gated on some IPs.

   Transcripts are therefore **best-effort**: when both paths are gated for a given video/IP, no transcript item is emitted (the video and comments are unaffected). A **residential proxy** materially improves the hit rate.
4. **Comments** – paginates the InnerTube `next` endpoint, joining the thread structure with the `frameworkUpdates` entity payloads to recover the real text/author/likes.

> ⚠️ **Use a residential proxy.** YouTube rate-limits datacenter IPs quickly (HTTP 429s and empty responses), especially for comment pagination. The default proxy group is `RESIDENTIAL` for this reason — and residential bandwidth is the main cost driver of a run.

### Example input

```json
{
  "videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
  "searchQueries": ["lofi hip hop"],
  "maxResultsPerSearch": 25,
  "scrapeVideoMetadata": true,
  "scrapeTranscript": true,
  "scrapeComments": true,
  "maxComments": 100,
  "includeCommentReplies": false,
  "commentsSortBy": "top",
  "language": "en",
  "preferManualCaptions": true,
  "emitSearchResults": false,
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
````

### Example output

Each item carries a `type` field so you can split the dataset by record type. A video that is private, members-only, age-restricted, region-blocked, or not playable is emitted as a `type: "skipped"` item (with a `reason`) instead of crashing the run.

**Video** (`type: "video"`)

```json
{
  "type": "video",
  "videoId": "dQw4w9WgXcQ",
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
  "description": "The official video for \"Never Gonna Give You Up\" by Rick Astley...",
  "channelName": "Rick Astley",
  "channelId": "UCuAXFkgsw1L7xaCfnd5JJOw",
  "channelUrl": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
  "viewCount": 1600000000,
  "likeCount": 17000000,
  "publishDate": "2009-10-25",
  "uploadDate": "2009-10-25",
  "durationSeconds": 212,
  "category": "Music",
  "keywords": ["rick astley", "never gonna give you up"],
  "isLiveContent": false,
  "isFamilySafe": true,
  "thumbnails": [{ "url": "https://i.ytimg.com/...", "width": 1280, "height": 720 }],
  "availableCaptionLanguages": [{ "languageCode": "en", "name": "English", "isAutoGenerated": false }],
  "commentCountApprox": 2200000,
  "foundViaQuery": null,
  "scrapedAt": "2026-06-26T10:00:00.000Z"
}
```

**Transcript** (`type: "transcript"`)

```json
{
  "type": "transcript",
  "videoId": "dQw4w9WgXcQ",
  "language": "en",
  "isAutoGenerated": false,
  "trackName": "English",
  "fullText": "We're no strangers to love ...",
  "segments": [{ "start": 18.5, "dur": 3.2, "text": "We're no strangers to love" }],
  "scrapedAt": "2026-06-26T10:00:00.000Z"
}
```

**Comment** (`type: "comment"`)

```json
{
  "type": "comment",
  "videoId": "dQw4w9WgXcQ",
  "commentId": "UgxKREWxIgDrw8w2e_Z4AaABAg",
  "text": "Still a banger in 2026.",
  "authorName": "@someuser",
  "authorChannelId": "UCabc...",
  "authorChannelUrl": "https://www.youtube.com/channel/UCabc...",
  "authorThumbnail": "https://yt3.ggpht.com/...",
  "likeCount": 1200,
  "replyCount": 8,
  "publishedTimeText": "3 weeks ago",
  "isPinned": false,
  "isHearted": false,
  "isReply": false,
  "parentCommentId": null,
  "scrapedAt": "2026-06-26T10:00:00.000Z"
}
```

**Search result** (`type: "searchResult"`, only when **Also emit raw search results** is enabled)

```json
{
  "type": "searchResult",
  "query": "lofi hip hop",
  "videoId": "jfKfPfyJRdk",
  "url": "https://www.youtube.com/watch?v=jfKfPfyJRdk",
  "title": "lofi hip hop radio",
  "channelName": "Lofi Girl",
  "channelId": "UCSJ4gkVC6NrvII8umztf0Ow",
  "viewCountText": "Streaming",
  "publishedTimeText": null,
  "lengthText": null,
  "thumbnails": [{ "url": "https://i.ytimg.com/...", "width": 360, "height": 202 }],
  "scrapedAt": "2026-06-26T10:00:00.000Z"
}
```

### Output fields

**Video** (`type: "video"`)

| Field | Type | Description |
|---|---|---|
| `type` | string | Always `"video"`. |
| `videoId` | string | 11-character video ID. |
| `url` | string | Canonical watch URL. |
| `title` | string | Video title. |
| `description` | string | Full description text. |
| `channelName` | string | Channel (author) name. |
| `channelId` | string | Channel ID. |
| `channelUrl` | string | Channel URL. |
| `viewCount` | integer | Total views (nullable). |
| `likeCount` | integer | Approximate like count from the watch UI (nullable; YouTube does not expose an exact number). |
| `publishDate` | string | Publish date from microformat (nullable). |
| `uploadDate` | string | Upload date from microformat (nullable). |
| `durationSeconds` | integer | Duration in seconds (nullable). |
| `category` | string | Video category (nullable). |
| `keywords` | array | Tags array (may be empty). |
| `isLiveContent` | boolean | Whether the video is/was live (nullable). |
| `isFamilySafe` | boolean | Family-safe flag (nullable). |
| `thumbnails` | array | Array of `{ url, width, height }`. |
| `availableCaptionLanguages` | array | Array of `{ languageCode, name, isAutoGenerated }`. |
| `commentCountApprox` | integer | Approximate comment count from the header (nullable). |
| `foundViaQuery` | string | The search query that surfaced this video (only present for videos discovered via `searchQueries`). |
| `scrapedAt` | string | ISO 8601 timestamp. |

**Transcript** (`type: "transcript"`)

| Field | Type | Description |
|---|---|---|
| `type` | string | Always `"transcript"`. |
| `videoId` | string | Video ID. |
| `language` | string | Caption language code (nullable). |
| `isAutoGenerated` | boolean | `true` for auto (ASR) captions. |
| `trackName` | string | Human-readable track name (nullable). |
| `fullText` | string | All segments joined into one string. |
| `segments` | array | Array of `{ start, dur, text }` (seconds as floats; `start`/`dur` may be null). |
| `scrapedAt` | string | ISO 8601 timestamp. |

**Comment** (`type: "comment"`)

| Field | Type | Description |
|---|---|---|
| `type` | string | Always `"comment"`. |
| `videoId` | string | Video the comment belongs to. |
| `commentId` | string | Comment ID. |
| `text` | string | Comment body. |
| `authorName` | string | Author handle / display name. |
| `authorChannelId` | string | Author channel ID (nullable). |
| `authorChannelUrl` | string | Author channel URL (nullable). |
| `authorThumbnail` | string | Author avatar URL (nullable). |
| `likeCount` | integer | Like count (defaults to 0 when not shown). |
| `replyCount` | integer | Reply count (defaults to 0). |
| `publishedTimeText` | string | Relative time string (e.g. "3 weeks ago"); YouTube does not expose absolute comment dates. |
| `isPinned` | boolean | Whether the comment is pinned. |
| `isHearted` | boolean | Whether the creator hearted it. |
| `isReply` | boolean | `true` for replies. |
| `parentCommentId` | string | Parent comment ID (null for top-level). |
| `scrapedAt` | string | ISO 8601 timestamp. |

**Search result** (`type: "searchResult"`)

| Field | Type | Description |
|---|---|---|
| `type` | string | Always `"searchResult"`. |
| `query` | string | The search query that matched. |
| `videoId` | string | Video ID. |
| `url` | string | Watch URL. |
| `title` | string | Video title. |
| `channelName` | string | Channel name (nullable). |
| `channelId` | string | Channel ID (nullable). |
| `viewCountText` | string | View-count display string from the search card (nullable). |
| `publishedTimeText` | string | Relative publish-time display string (nullable). |
| `lengthText` | string | Duration display string (nullable). |
| `thumbnails` | array | Array of `{ url, width, height }`. |
| `scrapedAt` | string | ISO 8601 timestamp. |

**Skipped** (`type: "skipped"`)

| Field | Type | Description |
|---|---|---|
| `type` | string | Always `"skipped"`. |
| `videoId` | string | Video ID that could not be scraped. |
| `url` | string | Watch URL. |
| `reason` | string | Why it was skipped (e.g. `LOGIN_REQUIRED`, `AGE_VERIFICATION_REQUIRED`, `no_video_details`, `watch_fetch_failed: ...`). |
| `scrapedAt` | string | ISO 8601 timestamp. |

### Input reference

| Field | Type | Default | Description |
|---|---|---|---|
| `videoUrls` | array | – | YouTube watch URLs (`watch?v=`, `youtu.be/`, `/shorts/`, `/embed/`, `/live/`). Normalized to the 11-char ID. |
| `videoIds` | array | – | Raw 11-character video IDs. |
| `searchQueries` | array | – | Keyword search strings (videos only); matches are enriched like direct inputs. |
| `maxResultsPerSearch` | integer | 50 | Max videos per search query (pagination stops once reached). |
| `scrapeVideoMetadata` | boolean | true | Emit `type: "video"` items. |
| `scrapeTranscript` | boolean | true | Fetch transcript/captions (best-effort). |
| `scrapeComments` | boolean | false | Fetch comments (slowest, most rate-limited path). |
| `maxComments` | integer | 100 | Max top-level comments per video; 0 = unlimited. |
| `includeCommentReplies` | boolean | false | Also expand reply threads. |
| `commentsSortBy` | string | top | `"top"` or `"newest"`. |
| `language` | string | en | Preferred caption language code; also sets the InnerTube `hl` locale. |
| `preferManualCaptions` | boolean | true | Prefer human captions over auto-generated. |
| `emitSearchResults` | boolean | false | Also push raw `type: "searchResult"` items for searches. |
| `proxyConfiguration` | object | RESIDENTIAL | Apify proxy; RESIDENTIAL strongly recommended (and the default). |

### Common use cases

- **Transcript mining** – pull YouTube transcripts for summarization, search, or LLM/RAG pipelines.
- **Comment & sentiment analysis** – export top or newest comments (and replies) as a YouTube comments API for NLP and analysis.
- **Channel & topic research** – search a keyword and collect metadata across many videos at once.
- **Content monitoring** – track view counts, approximate likes, and comment volume over time.
- **Competitor & market research** – build datasets from competitor channels and export to JSON/CSV/Excel.

### FAQ

**Do I need a YouTube/Google API key?** No. This YouTube scraper uses YouTube's internal InnerTube endpoints — no API key, no OAuth, no quota.

**Why are some transcripts missing?** Transcripts are best-effort. Not every video has captions, and some only have auto-generated ones. YouTube also gates caption text behind PoToken/IP checks: the Actor first tries the InnerTube `get_transcript` panel endpoint via the iOS client (which still returns text on mobile clients) and falls back to the timedtext caption URL, but on a gated video/IP combination both can come back empty — in that case the video and comments are still scraped, just without a transcript. A residential proxy improves the YouTube transcript hit rate.

**Why are comments slow, and why do they need a proxy?** Comments are the slowest, most rate-limited path: the Actor paginates the InnerTube `next` endpoint one page at a time with jitter to stay under YouTube's limits, so a video with many comments takes proportionally longer. Datacenter IPs get 429s and empty responses quickly, so a residential proxy is strongly recommended when scraping comments at volume. Comments are off by default for this reason.

**Why do I need a residential proxy, and what does it cost?** Datacenter IPs get rate-limited (429s) and served empty responses, especially during comment pagination, so the default proxy group is `RESIDENTIAL`. Residential proxy bandwidth is the main cost driver of a run — fetching watch pages, comments, and transcripts all consume residential traffic — so scope your `maxComments` and `maxResultsPerSearch` to control spend.

**Which languages are supported?** Any language YouTube has a caption track for. Set `language` to a language code (e.g. `en`, `es`, `de`, `fr`, `pt`, `hi`, `ja`) to pick the caption track and the InnerTube `hl` locale; if that exact track is missing, the scraper falls back to another track or an auto-generated one. `preferManualCaptions` controls whether human captions win over ASR.

**How are limits and pagination handled?** `maxResultsPerSearch` caps videos per search query, and `maxComments` caps top-level comments per video (`0` = unlimited). Search and comments paginate automatically until the limit is hit or YouTube stops returning a continuation. Runs are resumable: a restarted run skips videos it already finished.

**What export formats are available?** The dataset can be exported to **JSON, CSV, or Excel** (also XML, JSONL, RSS, or HTML table) from the Apify Console, or fetched programmatically via the Apify API.

***

This YouTube scraper pairs well with our other social and video scrapers (TikTok, Instagram, Reddit, and more) when you need cross-platform data.

# Actor input Schema

## `videoUrls` (type: `array`):

YouTube watch URLs to scrape. Accepts watch?v=, youtu.be/, /shorts/ and /embed/ forms; each is normalized to its 11-character video ID. You can paste plain strings or { "url": "..." } objects.

## `videoIds` (type: `array`):

Raw 11-character YouTube video IDs to scrape directly, e.g. "dQw4w9WgXcQ" (alternative to Video URLs).

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

Keyword search strings. Each runs a YouTube search (videos only); matched videos are then enriched exactly like direct inputs.

## `maxResultsPerSearch` (type: `integer`):

Maximum number of videos to collect per search query (pagination stops once reached).

## `scrapeVideoMetadata` (type: `boolean`):

Emit a `type: "video"` item per video (title, channel, views, publish date, duration, keywords, thumbnails, available caption languages, etc.).

## `scrapeTranscript` (type: `boolean`):

Fetch the transcript (full text plus timestamped segments). Best-effort: uses YouTube's InnerTube transcript-panel endpoint via the mobile client, with the timedtext caption URL as a fallback. YouTube gates caption text behind PoToken/IP checks, so a transcript may be unavailable for some videos/IPs even when captions exist — a residential proxy improves the hit rate.

## `scrapeComments` (type: `boolean`):

Fetch comments. This is the slowest, most rate-limited path, so it is off by default. A residential proxy is strongly recommended when enabled.

## `maxComments` (type: `integer`):

Maximum top-level comments per video. Set 0 for unlimited (will paginate until YouTube stops returning a continuation).

## `includeCommentReplies` (type: `boolean`):

Also expand reply threads (one extra request per thread that has replies). Replies are emitted as comment items with `isReply: true` and a `parentCommentId`.

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

Which comment ordering to request from YouTube.

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

Preferred caption language code (e.g. "en", "es", "de"). The matching caption track is selected when available; otherwise the scraper falls back to another track or an auto-generated one. Also sets the InnerTube `hl` locale.

## `preferManualCaptions` (type: `boolean`):

Prefer human-written (non-ASR) captions over auto-generated ones when both exist for the chosen language.

## `emitSearchResults` (type: `boolean`):

When using search queries, also push a lightweight `type: "searchResult"` item for each matched video (query, title, channel, view-count text, etc.) in addition to the enriched video/transcript/comment items.

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

YouTube rate-limits datacenter IPs aggressively (429s and empty responses), so a RESIDENTIAL proxy is strongly recommended, especially for comments at volume.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://youtu.be/9bZkp7q19f0"
  ],
  "videoIds": [
    "dQw4w9WgXcQ"
  ],
  "searchQueries": [
    "lofi hip hop",
    "machine learning tutorial"
  ],
  "maxResultsPerSearch": 50,
  "scrapeVideoMetadata": true,
  "scrapeTranscript": true,
  "scrapeComments": false,
  "maxComments": 100,
  "includeCommentReplies": false,
  "commentsSortBy": "top",
  "language": "en",
  "preferManualCaptions": true,
  "emitSearchResults": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

No description

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

No description

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

No description

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

No description

## `skipped` (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 = {
    "videoUrls": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    ],
    "maxResultsPerSearch": 50,
    "maxComments": 100,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("eccentric_layout/youtube-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 = {
    "videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
    "maxResultsPerSearch": 50,
    "maxComments": 100,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

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

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "maxResultsPerSearch": 50,
  "maxComments": 100,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call eccentric_layout/youtube-scraper --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "YouTube Scraper (Videos, Comments, Transcripts)",
        "description": "Scrape YouTube without an API key: video metadata, comments, and transcripts/captions by video URL, video ID, or keyword search. Uses YouTube's InnerTube endpoints. Export JSON/CSV/Excel.",
        "version": "0.1",
        "x-build-id": "s88VWWalWzs6IiyUk"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/eccentric_layout~youtube-scraper/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-eccentric_layout-youtube-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/eccentric_layout~youtube-scraper/runs": {
            "post": {
                "operationId": "runs-sync-eccentric_layout-youtube-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/eccentric_layout~youtube-scraper/run-sync": {
            "post": {
                "operationId": "run-sync-eccentric_layout-youtube-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "videoUrls": {
                        "title": "Video URLs",
                        "type": "array",
                        "description": "YouTube watch URLs to scrape. Accepts watch?v=, youtu.be/, /shorts/ and /embed/ forms; each is normalized to its 11-character video ID. You can paste plain strings or { \"url\": \"...\" } objects.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "videoIds": {
                        "title": "Video IDs",
                        "type": "array",
                        "description": "Raw 11-character YouTube video IDs to scrape directly, e.g. \"dQw4w9WgXcQ\" (alternative to Video URLs).",
                        "items": {
                            "type": "string"
                        }
                    },
                    "searchQueries": {
                        "title": "Search queries",
                        "type": "array",
                        "description": "Keyword search strings. Each runs a YouTube search (videos only); matched videos are then enriched exactly like direct inputs.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "maxResultsPerSearch": {
                        "title": "Max results per search",
                        "minimum": 1,
                        "type": "integer",
                        "description": "Maximum number of videos to collect per search query (pagination stops once reached).",
                        "default": 50
                    },
                    "scrapeVideoMetadata": {
                        "title": "Scrape video metadata",
                        "type": "boolean",
                        "description": "Emit a `type: \"video\"` item per video (title, channel, views, publish date, duration, keywords, thumbnails, available caption languages, etc.).",
                        "default": true
                    },
                    "scrapeTranscript": {
                        "title": "Scrape transcript / captions",
                        "type": "boolean",
                        "description": "Fetch the transcript (full text plus timestamped segments). Best-effort: uses YouTube's InnerTube transcript-panel endpoint via the mobile client, with the timedtext caption URL as a fallback. YouTube gates caption text behind PoToken/IP checks, so a transcript may be unavailable for some videos/IPs even when captions exist — a residential proxy improves the hit rate.",
                        "default": true
                    },
                    "scrapeComments": {
                        "title": "Scrape comments",
                        "type": "boolean",
                        "description": "Fetch comments. This is the slowest, most rate-limited path, so it is off by default. A residential proxy is strongly recommended when enabled.",
                        "default": false
                    },
                    "maxComments": {
                        "title": "Max comments per video",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Maximum top-level comments per video. Set 0 for unlimited (will paginate until YouTube stops returning a continuation).",
                        "default": 100
                    },
                    "includeCommentReplies": {
                        "title": "Include comment replies",
                        "type": "boolean",
                        "description": "Also expand reply threads (one extra request per thread that has replies). Replies are emitted as comment items with `isReply: true` and a `parentCommentId`.",
                        "default": false
                    },
                    "commentsSortBy": {
                        "title": "Comments sort order",
                        "enum": [
                            "top",
                            "newest"
                        ],
                        "type": "string",
                        "description": "Which comment ordering to request from YouTube.",
                        "default": "top"
                    },
                    "language": {
                        "title": "Preferred caption language",
                        "type": "string",
                        "description": "Preferred caption language code (e.g. \"en\", \"es\", \"de\"). The matching caption track is selected when available; otherwise the scraper falls back to another track or an auto-generated one. Also sets the InnerTube `hl` locale.",
                        "default": "en"
                    },
                    "preferManualCaptions": {
                        "title": "Prefer manual captions",
                        "type": "boolean",
                        "description": "Prefer human-written (non-ASR) captions over auto-generated ones when both exist for the chosen language.",
                        "default": true
                    },
                    "emitSearchResults": {
                        "title": "Also emit raw search results",
                        "type": "boolean",
                        "description": "When using search queries, also push a lightweight `type: \"searchResult\"` item for each matched video (query, title, channel, view-count text, etc.) in addition to the enriched video/transcript/comment items.",
                        "default": false
                    },
                    "proxyConfiguration": {
                        "title": "Proxy configuration",
                        "type": "object",
                        "description": "YouTube rate-limits datacenter IPs aggressively (429s and empty responses), so a RESIDENTIAL proxy is strongly recommended, especially for comments at volume.",
                        "default": {
                            "useApifyProxy": true,
                            "apifyProxyGroups": [
                                "RESIDENTIAL"
                            ]
                        }
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
