# YouTube Scraper : Videos, Channels & Comments (`techforce.global/youtube-scraper`) Actor

Scrape YouTube videos, channels & comments by keyword, hashtag, or URL, no API key or quota limits. Get titles, views, likes, upload dates, channel stats, and comments as clean JSON

- **URL**: https://apify.com/techforce.global/youtube-scraper.md
- **Developed by:** [Techforce Global](https://apify.com/techforce.global) (community)
- **Categories:** Agents, Social media, Videos
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $9.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## YouTube Data API Alternative - Videos, Channels & Comments

[![Apify Actor](https://img.shields.io/badge/Apify-Actor-orange?logo=apify)](https://apify.com/techforce.global/youtube-scraper)
[![MCP Ready](https://img.shields.io/badge/MCP-Compatible-blue?logo=anthropic)](https://mcp.apify.com?tools=techforce.global/youtube-scraper)
[![Pricing](https://img.shields.io/badge/Pricing-Usage--based-green)](https://apify.com/techforce.global/youtube-scraper)
[![Inputs](https://img.shields.io/badge/Inputs-search%20%C2%B7%20hashtag%20%C2%B7%20channel%20%C2%B7%20video-brightgreen)](#-input-contract)
[![Output](https://img.shields.io/badge/Output-video%20%C2%B7%20channel%20%C2%B7%20comment-darkgreen)](#-output-contract--data-structure)
[![Target](https://img.shields.io/badge/Target-YouTube.com-red)](https://www.youtube.com)

> **No API key, no Google Cloud project, no daily quota.** Search terms, hashtags, channel URLs or
> video URLs in — video metadata, channel stats and full comment threads out, as clean JSON in a
> single dataset. Built for content research, competitor tracking and audience analysis, with
> optional delivery into Notion, Slack, Linear, Jira, GitHub or Airtable over MCP.

#### 🔐 Trust & compliance at a glance

| | |
| :--- | :--- |
| **Credentials** | None required. No Google or YouTube account, and delivery runs through the Apify MCP Proxy, so the Actor never sees your connector tokens. |
| **Data scope** | **Public content only** — public videos, channel pages and comment threads. No login, no private or members-only content. |
| **⚠️ Personal data** | **The one Actor in this family that collects it.** `author`, `authorId` and `text` on comment rows identify real people, and `authorId` is a stable identifier. **`scrapeComments` is `true` by default.** |
| **Opt out in one field** | `scrapeComments: false` (or `maxComments: 0`) gives a video/channel-only dataset with no personal data. |
| **Snapshot honesty** | Counts are true as of `scrapedAt` (ISO-8601 UTC); hidden counts come back `null`, never `0`. |

Full detail: **[Compliance & Data Privacy](#-compliance--data-privacy)**.

***

#### Official YouTube Data API v3 vs. YouTube Scraper Actor

The official API is a genuine alternative with genuine limits — the quota is the reason this Actor
exists:

| Feature / Capability | Official YouTube Data API v3 | YouTube Scraper Actor (Apify) |
| :--- | :--- | :--- |
| **Setup** | ❌ Google Cloud project → enable API → key/OAuth | ✅ Apify account, nothing else |
| **Daily quota** | ❌ 10,000 units/day, and a **single search costs 100 units** — roughly 100 searches before you're locked out | ✅ No quota |
| **Quota reset** | ❌ Midnight Pacific, whether you're mid-job or not | ✅ N/A |
| **Comment threads** | ⚠️ Available, but each call comes out of the same daily budget | ✅ Up to 1,000 per video, no budget |
| **Hashtag input** | ❌ No hashtag concept — you'd search the text yourself | ✅ Pass hashtags directly; run as a `#tag` search |
| **Officially sanctioned** | ✅ Yes | ⚠️ No — see [Compliance](#-compliance--data-privacy) |
| **Stability** | ✅ Versioned contract | ⚠️ Depends on YouTube's page structure |
| **Cost model** | Free until you hit the wall, then blocked | Usage-based, no wall |

**Use the official API if your volume fits inside the quota.** Use this Actor when the quota is the
thing standing in your way.

> Quota figures are Google's to change — check
> [Google's current quota docs](https://developers.google.com/youtube/v3/determine_quota_cost) if
> this comparison is load-bearing.

***

### 🤖 AI Discovery & MCP Metadata

Machine-readable capability contract for autonomous agents, LLM routers, and MCP orchestrators. Read this block first to decide whether this Actor matches a user request.

```yaml
ai_discovery:
  slug: "techforce.global/youtube-scraper"
  one_line: >
    Scrapes YouTube videos, channels and comments from search terms, hashtags, channel URLs or
    video URLs without a Data API key or quota. Three record types in one dataset - video
    metadata, channel stats and comment threads. Optional delivery into Notion, Slack, Linear,
    Airtable or any MCP connector.
  primary_purpose: "extract"
  secondary_purpose: "deliver"
  entity: "YouTube video, channel and comment"
  category: "social-media / video-analytics / content-research / audience-insight"
  supported_platforms: ["youtube.com", "youtu.be"]
  supported_tasks:
    - "search-videos-by-keyword"
    - "search-videos-by-hashtag"
    - "list-a-channels-videos-and-stats"
    - "get-metadata-for-specific-video-urls"
    - "scrape-comment-threads-with-replies"
    - "deliver-results-to-mcp-connector"
  unsupported_tasks:
    - "get-transcripts-or-captions (use youtube-transcript-scraper or youtube-full-channel-transcripts-scraper)"
    - "list-a-channels-shorts-specifically (use youtube-shorts-scraper)"
    - "download-video-or-audio-files"
    - "creator-analytics-watch-time-retention-revenue (not public)"
    - "viewer-demographics (not public)"
    - "post-comments-upload-or-any-write-action"
    - "scrape-private-unlisted-or-members-only-content"
  search_keywords:
    # First entry is the H1 / meta-description target. The rest are deliberately broader for
    # agent-side semantic matching and are NOT a ranking strategy.
    - "youtube data api alternative"
    - "youtube scraper"
    - "youtube comment scraper"
    - "scrape youtube search results"
    - "youtube channel statistics scraper"
    - "youtube hashtag scraper"
    - "youtube api quota workaround"
    - "audience sentiment youtube"
    - "mcp youtube tool"

  input_entities:
    - "searchQueries (array of search terms)"
    - "hashtags (array, with or without #; run as a '#tag' search)"
    - "channelUrls (array of channel URLs)"
    - "videoUrls (array of watch URLs)"
    - "maxVideos (1-500, PER search/hashtag/channel)"
    - "maxComments (0-1000, PER video)"
    - "scrapeComments (boolean, DEFAULT TRUE - collects personal data)"
    - "scrapeChannelInfo (boolean, default true)"
    - "proxyConfiguration (Apify Proxy on by default - required in practice)"

  no_input_field_is_required_by_the_schema: >
    CRITICAL: none of the four input arrays is marked required, so a run with none of them passes
    validation, logs "No input provided..." and ends SUCCEEDED with an empty dataset. Supply at
    least one of searchQueries, hashtags, channelUrls or videoUrls.

  output_is_three_record_types: >
    CRITICAL: one dataset holds three shapes, discriminated by the `type` field - "video",
    "channel" and "comment", each plus scrapedAt. Always branch on `type` before reading a row.

  output_entities_video: ["type", "source", "videoId", "url", "title", "description", "channelId",
    "channelName", "channelUrl", "viewCount", "likeCount", "commentCount", "duration",
    "durationStr", "thumbnail", "isLive", "isShort", "tags", "categories", "language",
    "uploadDate", "scrapedAt"]
  output_entities_channel: ["type", "channelId", "channelName", "channelUrl", "description",
    "subscriberCount", "videoCount", "thumbnail", "country", "scrapedAt"]
  output_entities_comment: ["type", "videoId", "commentId", "text", "author", "authorId", "likes",
    "replyCount", "isReply", "publishedAt", "scrapedAt"]

  comments_contain_personal_data: >
    author, authorId and text identify real people and are personal data under GDPR/UK GDPR and
    CCPA-CPRA. authorId is a stable identifier for an individual. scrapeComments defaults to TRUE.

  is_short_is_unreliable: >
    isShort is true only when the video's own page URL was a /shorts/ URL AND duration <= 60s. A
    Short reached through search, a channel listing or a /watch?v= URL - which is how this Actor
    reaches almost everything - typically reports false. Filter on duration <= 60 yourself, or use
    techforce.global/youtube-shorts-scraper.

  item_count_multiplies_fast: >
    maxVideos is PER search/hashtag/channel and maxComments is PER video. Two searches at
    maxVideos 20 with maxComments 50 is up to 40 videos + 2000 comments = 2040 dataset items.

  no_deduplication: >
    The same video matched by two inputs is written twice with a different `source`. Dedupe on
    videoId downstream if that matters.

  proxy_is_required_in_practice: >
    YouTube blocks Apify's platform IPs with "Sign in to confirm you're not a bot".
    proxyConfiguration defaults to Apify Proxy - leave it enabled. A fresh IP rotates in per request.

  known_limitations:
    like_count_often_null: "many creators hide likes; null is not zero"
    comment_count_vs_scraped: "commentCount is what YouTube reports, not how many rows were scraped"
    description_truncated: "video and channel description fields are cut to 2000 characters"
    publishedAt_is_relative: "comment publishedAt is relative text such as '3 weeks ago'; scrapedAt is ISO-8601 UTC"
    upload_date_normalised: "uploadDate is YYYY-MM-DD (raw YYYYMMDD only if unparseable)"
    no_transcripts: "use the transcript Actors"
    no_video_files: "metadata and thumbnail URLs only"
    snapshot_only: "counts are true as of scrapedAt"
    free_plan_capped: "free Apify plans stop at 5 items TOTAL per run"

  pricing:
    model: "usage-based"
    billing_unit: "Apify platform usage consumed by the run"
    hard_ceiling_parameter: "maxTotalChargeUsd"
    note: "Current rates are on the Actor's Apify Store page; the Console shows a live total mid-run."

  when_to_use: >
    You need YouTube video, channel or comment data at a volume the Data API quota will not
    accommodate, or you want search, hashtag, channel and video inputs mixed in one run.
  when_not_to_use: >
    You need transcripts, a channel's Shorts specifically, the video files, creator analytics,
    viewer demographics, or to post or upload anything.

  mcp_delivery:
    modes: ["summary", "chunked", "perItem", "none"]
    default: "summary"
    summary_and_chunked_placeholders: ["{itemCount}", "{itemsText}", "{message}"]
    chunked_only_placeholders: ["{part}", "{partCount}"]
    per_item_covers_every_row: >
      Delivery reads the whole dataset after scraping, so perItem issues one connector call per
      row - comments included. A 2000-comment run is 2000 calls. Use chunked or summary.
    per_item_shape_varies: >
      In perItem mode the placeholders depend on the record's `type`. A template referencing
      {title} renders empty for a comment row. Branch on {type}, or use chunked/summary.
    never_blocks_the_dataset: >
      Delivery runs after the dataset is saved. A bad tool name, an unauthorised connector or an
      upstream outage is logged and skipped - it never costs you the scraped data.

  alternatives:
    - "techforce.global/youtube-transcript-scraper for one video's timestamped transcript"
    - "techforce.global/youtube-full-channel-transcripts-scraper for a whole channel's transcripts"
    - "techforce.global/youtube-shorts-scraper for a channel's Shorts and optional media download"
```

***

```json
{
  "@context": "https://schema.org/",
  "@type": "SoftwareApplication",
  "name": "YouTube Scraper – Videos, Channels & Comments",
  "alternateName": "YouTube Data API Alternative",
  "applicationCategory": "BusinessApplication",
  "operatingSystem": "Cloud (Apify platform)",
  "description": "Scrapes YouTube videos, channels and comments from search terms, hashtags, channel URLs or video URLs without a YouTube Data API key, Google Cloud project, or daily quota. Returns three record types in one dataset — video metadata with view, like and comment counts; channel stats including subscribers; and comment threads with author, likes and reply flags — each discriminated by a type field. Public content only, with comment scraping switchable off in one field for callers who do not want personal data. Apify Proxy rotates a fresh IP per request, and results can be delivered into Notion, Slack, Linear, Airtable or any MCP connector.",
  "url": "https://apify.com/techforce.global/youtube-scraper",
  "offers": {
    "@type": "Offer",
    "priceCurrency": "USD",
    "category": "Pay per platform usage",
    "description": "Usage-based: billed for the compute time and resources a run consumes, not per row. Current rates are shown on the Actor's Apify Store page."
  }
}
```

***

### What this Actor does

> 💡 **Words, Shorts or transcripts?** This Actor returns **numbers and comments**. For what was
> *said*, use **[YouTube Transcript Scraper](https://apify.com/techforce.global/youtube-transcript-scraper)**
> (one video) or
> **[Full Channel Transcripts Scraper](https://apify.com/techforce.global/youtube-full-channel-transcripts-scraper)**
> (a whole channel). For a channel's Shorts and their video files, use
> **[YouTube Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper)**.

- **Four ways in, no key required.** Search terms, hashtags, channel URLs and video URLs, mixed
  freely in one run. No Google Cloud project, no OAuth, no daily quota.

- **Three record types in one dataset.** `video`, `channel` and `comment`, discriminated by `type`.
  Comments carry `videoId`, so they join straight back to their video.

- **Duration twice.** `duration` in seconds for arithmetic, `durationStr` as `H:MM:SS` / `M:SS`
  for display.

- **Records which input produced each video.** `source` is `search`, `channel` or `direct_url`, so
  a run spanning three searches and two channels stays separable.

- **Normalises the dates that can be normalised, and says so when they can't.** `uploadDate` is
  `YYYY-MM-DD`, `scrapedAt` is ISO-8601 UTC, and a comment's `publishedAt` is YouTube's own
  relative text (`"3 weeks ago"`) because that's all YouTube shows.

- **Rotates a fresh proxy IP per request.** YouTube blocks Apify's platform IPs outright, so this
  isn't optional polish — it's what makes the Actor work at all.

- **Keeps going when one item fails.** An unextractable video or a comment thread that won't load
  is logged and skipped; the run continues. Rows are saved as it goes, so an aborted run keeps
  what it collected.

- **Lets you opt out of personal data in one field.** `scrapeComments: false` gives a clean
  metadata-only dataset.

- **Optionally delivers into your tools.** Notion, Slack, Linear, Jira, GitHub, Airtable or
  anything else you've authorised.

***

### Which YouTube Actor do I want?

| You want… | Actor |
|---|---|
| **Search, hashtags, channel stats, video metadata, comment threads** | **This Actor** |
| **One video's transcript**, timestamped, with translation | [YouTube Transcript Scraper](https://apify.com/techforce.global/youtube-transcript-scraper) |
| **Transcripts of every video** on a channel | [Full Channel Transcripts Scraper](https://apify.com/techforce.global/youtube-full-channel-transcripts-scraper) |
| A channel's **Shorts**, with engagement and optional **file download** | [YouTube Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper) |

None need a YouTube Data API key or a quota. They're complementary, not alternatives.

***

### When to use it — and when not to

| Scenario | Fit |
|---|---|
| Videos for a search term or hashtag | ✅ Strong — `searchQueries`, `hashtags` (run as a `#tag` search) |
| A channel's videos and channel-level stats | ✅ Strong — `channelUrls` |
| Metadata for specific videos | ✅ Strong — `videoUrls` |
| Comment threads with replies | ✅ Strong — up to 1,000 per video, `isReply` marks replies |
| Audience sentiment / social listening | ✅ Strong — comment text, likes, reply counts |
| Skipping the Data API quota entirely | ✅ Strong — the main reason to use this |
| View, like and subscriber counts | ✅ Strong — `likeCount` / `subscriberCount` often null (hidden) |
| Metadata **without** personal data | ✅ Strong — `scrapeComments: false` |
| Results in Notion / Slack / Airtable | ✅ Strong — native MCP delivery |
| Telling Shorts from long-form | ⚠️ `isShort` under-reports — filter `duration <= 60`, or use the [Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper) |
| A guaranteed-complete comment thread | ⚠️ Capped at 1,000/video; YouTube paginates its own way |
| A deduplicated video list across inputs | ⚠️ No dedup — same video can appear twice with different `source` |
| **Transcripts, captions or subtitles** | ❌ Use the [Transcript](https://apify.com/techforce.global/youtube-transcript-scraper) or [Full Channel](https://apify.com/techforce.global/youtube-full-channel-transcripts-scraper) Scraper |
| **Downloading video or audio files** | ❌ Metadata and thumbnail URLs only |
| **Creator analytics** — watch time, retention, revenue | ❌ Not public |
| Viewer demographics | ❌ Not public |
| **Historical** view-count series from one run | ❌ Snapshot per run — schedule to build history |
| Posting comments, uploads, playlist management | ❌ Read-only |
| Private, unlisted or members-only content | ❌ Public content only |

***

### ⚡ Execution Rules (Avoid Common Agent Failures)

1. **Supply at least one of `searchQueries`, `hashtags`, `channelUrls`, `videoUrls`.** **None is
   marked required in the schema**, so a run with none of them passes validation, logs
   `No input provided. Supply at least one of: searchQueries, channelUrls, videoUrls, hashtags.`
   and ends **`SUCCEEDED` with an empty dataset**. Never read `itemCount: 0` as a crash.

2. **Keep the proxy enabled.** Default is `{"useApifyProxy": true}`. Without it YouTube returns
   *"Sign in to confirm you're not a bot"* and you get nothing — the log warns
   `No proxy configured — YouTube is likely to block requests…`.

3. **Item counts multiply.** `maxVideos` is **per** search, hashtag and channel; `maxComments` is
   **per video**. Two searches at `maxVideos: 20` with `maxComments: 50` is up to **2,040 items**.

4. **`scrapeComments` is `true` by default** — your first run collects personal data unless you set
   it `false` (or `maxComments: 0`). Metadata-only is faster, cheaper, and out of scope for most of
   [Compliance](#-compliance--data-privacy).

5. **Branch on `type` before reading any row.** One dataset, three shapes. A consumer that assumes
   every row has `title` breaks on the first comment.

6. **`isShort` is a hint, not a filter.** True only when the page URL was a `/shorts/` URL **and**
   duration ≤ 60s — rarely how this Actor reaches a video. **Shorts found via search or a channel
   typically report `false`.** Filter on `duration <= 60`, or use the
   [Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper).

7. **`likeCount` and `subscriberCount` are frequently `null`** — hidden, not zero. Never average
   them as zero.

8. **`commentCount` ≠ the number of comment rows.** It's YouTube's figure; your rows are capped by
   `maxComments` and by what YouTube served.

9. **Know which dates are which.** `uploadDate` = `YYYY-MM-DD`, `scrapedAt` = ISO-8601 UTC, a
   comment's `publishedAt` = **relative text**. Sort comments by scrape order, not `publishedAt`.

10. **`description` is truncated at 2,000 characters** on video and channel rows.

11. **No deduplication.** A video matched by two inputs is written twice with a different `source`.

12. **Inputs run in a fixed order:** searches → hashtags → channels → video URLs. A run that stops
    early keeps what came first.

13. **`perItem` delivery issues one connector call per dataset row — comments included.** Delivery
    reads the whole dataset, so a 2,000-comment run is 2,000 calls. Use `chunked` or `summary`.
    In `perItem`, placeholders depend on `type`; `{title}` renders empty for comment rows.

14. **`deliveryMode` defaults to `summary`, not `none`.** For dataset-only runs, leave
    `mcpConnector` empty or set `deliveryMode: "none"`.

15. **Free Apify plans stop at 5 items *total* per run** — not per input.

```yaml
dependencies:
  - rule: "at least one of searchQueries / hashtags / channelUrls / videoUrls must be non-empty"
    on_violation: "run ends SUCCEEDED with an empty dataset and an error line in the log - it does NOT fail validation"
  - rule: "delivery requires mcpConnector AND mcpTool"
    on_violation: "delivery is skipped with a warning; every dataset row is still written"
  - rule: "mcpArguments must reference {message} for the rendered template to appear anywhere"
    on_violation: "message body is empty; the tool is still called"
  - rule: "mcpConnector only resolves when the Actor runs on the Apify platform"
    on_violation: "local runs log 'APIFY_MCP_PROXY_URL is not set' and skip delivery"
authentication:
  actor_input: none
  platform: "Apify API token required for API / SDK / MCP invocation"
  target_site: "no YouTube or Google account, API key, or OAuth app required (public content only)"
```

***

### 📥 Input Contract

`.actor/input_schema.json` is authoritative — if this page disagrees with the schema files,
**the schema files win**.

| Parameter | Type | Required | Default | Allowed values / format |
|---|---|---|---|---|
| `searchQueries` | array\<string> | see note | — | Search terms, each looked up separately |
| `hashtags` | array\<string> | see note | — | With or without `#`; run as a `#tag` search |
| `channelUrls` | array\<string> | see note | — | Channel URLs, e.g. `https://www.youtube.com/@mkbhd` |
| `videoUrls` | array\<string> | see note | — | Watch URLs. Scraped directly, no search |
| `maxVideos` | integer | ⬜ No | `20` | `1`–`500`, **per search / hashtag / channel** |
| `maxComments` | integer | ⬜ No | `50` | `0`–`1000`, **per video**. `0` disables comments |
| `scrapeComments` | boolean | ⬜ No | **`true`** | **Collects personal data when on.** `false` = metadata only |
| `scrapeChannelInfo` | boolean | ⬜ No | `true` | Emit a `channel` record for channel URLs |
| `proxyConfiguration` | object | ⬜ No | `{"useApifyProxy": true}` | Apify proxy editor object. **Leave enabled.** |

> **At least one of the four input arrays must be non-empty.** None is marked `required` in the
> schema, so this is enforced at runtime: a run without any of them succeeds with an empty dataset
> and an error line in the log.

#### What each input produces

| Input | Records emitted per item |
|---|---|
| `searchQueries` | up to `maxVideos` × `video` (`source: "search"`), each optionally followed by its `comment` rows |
| `hashtags` | the same, searched as `#tag` |
| `channelUrls` | one `channel` row (if `scrapeChannelInfo`), then up to `maxVideos` × `video` (`source: "channel"`) + comments |
| `videoUrls` | one `video` row (`source: "direct_url"`) plus its `comment` rows |

#### Delivery (optional)

| Parameter | Type | Default | Description |
|---|---|---|---|
| `mcpConnector` | string | `""` | A connector you've authorised on your Apify account — Notion, Slack, Linear, Jira, GitHub, Airtable, or any MCP-compatible connector. Leave empty for dataset-only runs. |
| `deliveryMode` | string (enum) | `"summary"` | `summary` (one call) · `chunked` (split across calls) · `perItem` (one call per row) · `none`. |
| `mcpTool` | string | `""` | Tool to call — `create_page`, `send_message`, `create_issue`. **Required** when a connector is set; run once and the log lists the connector's tools. |
| `mcpArguments` | object | `{}` | Arguments for the tool. String leaves support `{placeholder}` tokens. |
| `mcpMessageTemplate` | string | `""` | Template rendered and exposed to `mcpArguments` as `{message}`. |

**Placeholders by mode:**

| Mode | Placeholders |
|---|---|
| `summary` | `{itemCount}` `{itemsText}` (formatted list of every result) `{message}` |
| `chunked` | the same, plus `{part}` `{partCount}` — `{itemsText}` holds one part |
| `perItem` | `{type}` `{scrapedAt}` `{message}` `{itemCount}`, plus **whichever fields that record type carries** — video: `{videoId}` `{title}` `{url}` `{viewCount}` `{likeCount}` `{duration}` `{tags}` …; channel: `{channelName}` `{subscriberCount}` `{videoCount}` …; comment: `{author}` `{text}` `{likes}` `{isReply}` … |

Unknown tokens render empty rather than erroring — which is why `{type}` matters in `perItem`.
`chunked` groups results so no single call exceeds roughly 72,000 characters, keeping Notion and
similar services from rejecting the payload.

***

### 📤 Output Contract & Data Structure

**One dataset, three record types.** Branch on `type` first. Every row carries `scrapedAt`
(ISO-8601 UTC).

#### `type: "video"`

| Field | Type | Nullable | Meaning |
|---|---|---|---|
| `type` | string | No | Always `"video"`. |
| `source` | string | No | Which input produced it: `search`, `channel` or `direct_url`. |
| `videoId` | string | Yes | YouTube's 11-character ID. **The join key** for comments. |
| `url` | string | Yes | Canonical watch URL. |
| `title` | string | Yes | Video title. |
| `description` | string | Yes | **Truncated to 2,000 characters.** |
| `channelId` / `channelName` / `channelUrl` | string | Yes | Channel identity. |
| `viewCount` | integer | Yes | Views as of `scrapedAt`. |
| `likeCount` | integer | **Yes** | **Frequently null** — creators hide it. Null ≠ zero. |
| `commentCount` | integer | Yes | What YouTube reports — **not** the number of comment rows scraped. |
| `duration` | integer | Yes | Length in seconds. |
| `durationStr` | string | Yes | `H:MM:SS`, or `M:SS` under an hour. |
| `thumbnail` | string | Yes | Highest-resolution thumbnail available. |
| `isLive` | boolean | No | `true` for live streams. |
| `isShort` | boolean | No | **Unreliable — see the caveat below.** |
| `tags` / `categories` | array | No | `[]` when none. |
| `language` | string | Yes | Language code YouTube reports. |
| `uploadDate` | string | Yes | **`YYYY-MM-DD`** (raw `YYYYMMDD` only if unparseable). |
| `scrapedAt` | string | No | ISO-8601 UTC capture time. |

> **`isShort` caveat.** True only when the video's own page URL was a `/shorts/` URL **and**
> duration ≤ 60 seconds. Videos reached through search, a channel listing or a `/watch?v=` URL —
> nearly everything this Actor returns — normally report `false` even when they are Shorts. Filter
> on `duration <= 60`, or use the
> [YouTube Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper), which reads
> the Shorts tab directly.

#### `type: "channel"`

| Field | Type | Nullable | Meaning |
|---|---|---|---|
| `type` | string | No | Always `"channel"`. |
| `channelId` / `channelName` / `channelUrl` | string | Yes | Channel identity. |
| `description` | string | Yes | **Truncated to 2,000 characters.** |
| `subscriberCount` | integer | **Yes** | Null when the channel hides it. |
| `videoCount` | integer | Yes | Public video count as YouTube reports it. |
| `thumbnail` | string | Yes | Channel avatar URL. |
| `country` | string | Yes | Declared country. Often null. |
| `scrapedAt` | string | No | ISO-8601 UTC capture time. |

#### `type: "comment"` — contains personal data

| Field | Type | Nullable | Meaning |
|---|---|---|---|
| `type` | string | No | Always `"comment"`. |
| `videoId` | string | No | **Join key** back to the video row. |
| `commentId` | string | Yes | YouTube's comment ID. |
| `text` | string | Yes | Comment body. **Personal data.** |
| `author` | string | Yes | Display name. **Personal data.** |
| `authorId` | string | Yes | Stable channel identifier for that person. **Personal data.** |
| `likes` | integer | Yes | Likes on the comment. |
| `replyCount` | integer | Yes | Replies YouTube reports. |
| `isReply` | boolean | No | `true` when the row is a reply. |
| `publishedAt` | string | Yes | **Relative text** — e.g. `"3 weeks ago"`. Not a date. |
| `scrapedAt` | string | No | ISO-8601 UTC capture time. |

#### Example output — one of each type

```json
[
  {
    "type": "video",
    "source": "search",
    "videoId": "aBcDeFgHiJk",
    "url": "https://www.youtube.com/watch?v=aBcDeFgHiJk",
    "title": "How We Rebuilt Our Ingestion Pipeline",
    "description": "Everything we learned moving 40M events a day…",
    "channelId": "UC000000000000000000000",
    "channelName": "Practical Systems",
    "channelUrl": "https://www.youtube.com/@practicalsystems",
    "viewCount": 148203,
    "likeCount": null,
    "commentCount": 412,
    "duration": 964,
    "durationStr": "16:04",
    "thumbnail": "https://i.ytimg.com/vi/aBcDeFgHiJk/maxresdefault.jpg",
    "isLive": false,
    "isShort": false,
    "tags": ["data engineering", "kafka"],
    "categories": ["Science & Technology"],
    "language": "en",
    "uploadDate": "2026-06-14",
    "scrapedAt": "2026-08-27T09:12:44.183921+00:00"
  },
  {
    "type": "channel",
    "channelId": "UC000000000000000000000",
    "channelName": "Practical Systems",
    "channelUrl": "https://www.youtube.com/@practicalsystems",
    "description": "Weekly deep dives into systems that actually run in production.",
    "subscriberCount": 128000,
    "videoCount": 214,
    "thumbnail": "https://yt3.googleusercontent.com/%E2%80%A6",
    "country": null,
    "scrapedAt": "2026-08-27T09:12:41.006512+00:00"
  },
  {
    "type": "comment",
    "videoId": "aBcDeFgHiJk",
    "commentId": "UgxAbCdEf1234567890",
    "text": "The backpressure section finally made this click for me.",
    "author": "@some_viewer",
    "authorId": "UC111111111111111111111",
    "likes": 34,
    "replyCount": 2,
    "isReply": false,
    "publishedAt": "3 weeks ago",
    "scrapedAt": "2026-08-27T09:12:47.771044+00:00"
  }
]
```

> Values illustrate shape and formatting — the channel, video and commenter are invented. Note
> `likeCount: null` (that creator hid likes) and `publishedAt` as relative text.

#### What a successful run looks like

- **Status:** `SUCCEEDED`
- **Dataset:** interleaved `video`, `channel` and `comment` rows, in input order
- **Log:** one line per input (`Searching YouTube for: …`, `Scraping channel: …`), then
  `Scraping complete.`
- **Zero results is also a success.** No inputs supplied, a bot-block, or an unresolvable channel
  all return an empty dataset with `SUCCEEDED`. Check the log.
- **Per-item failures are logged and skipped** — `[yt-dlp] Error extracting …` or
  `[comments] Failed for …` — and the run continues.

#### Dataset views

| View | Shows |
|---|---|
| **Results overview** | Type, title, channel, views, likes, comments, duration, upload date |
| **Video detail** | Engagement, duration, live/Short flags, language, tags, categories, source |
| **Comments** | Video ID, author, text, likes, reply count and reply flag |
| **Channels** | Channel name, subscribers, video count, country, description |

***

### ▶️ Invocation & Integration

Authenticate every call with `Authorization: Bearer <APIFY_TOKEN>`.

#### Apify Console

Open the [Actor page](https://apify.com/techforce.global/youtube-scraper) → **Input** tab → fill in
at least one of search queries, hashtags, channel URLs or video URLs → set `maxVideos` and
`maxComments` → decide whether you want comments at all → leave the proxy on → **Start**.

#### Model Context Protocol (MCP)

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com/?tools=techforce.global/youtube-scraper"
```

Then ask your agent for *"the top videos and comment sentiment for this search term"*.

#### Python (apify-client)

```python
import os
from collections import Counter
from apify_client import ApifyClient

client = ApifyClient(os.getenv("APIFY_TOKEN"))

run = client.actor("techforce.global/youtube-scraper").call(run_input={
    "searchQueries": ["apify web scraping"],
    "maxVideos": 20,
    "scrapeComments": False,     # metadata only — no personal data
    "maxComments": 0,
    "deliveryMode": "none",
    "proxyConfiguration": {"useApifyProxy": True},
})

rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())

## An empty dataset is a SUCCEEDED run — check it before assuming failure.
if not rows:
    raise SystemExit("Nothing scraped — check the run log (no input? blocked?).")

print(Counter(r["type"] for r in rows))   # e.g. Counter({'video': 20})

## ALWAYS branch on type — one dataset, three shapes.
videos = [r for r in rows if r["type"] == "video"]

## likeCount is often null (creator hid it) — exclude, don't coerce to 0.
liked = [v for v in videos if v.get("likeCount") is not None]
for v in sorted(liked, key=lambda v: v["viewCount"] or 0, reverse=True)[:10]:
    rate = v["likeCount"] / v["viewCount"] if v.get("viewCount") else 0
    print(f'{v["viewCount"]:>10,}  {rate:>6.2%}  {v["title"][:60]}')

## isShort is unreliable through search — use duration instead.
print(f'{sum(1 for v in videos if (v.get("duration") or 999) <= 60)} likely Shorts by duration')
```

#### JavaScript / TypeScript (apify-client)

```typescript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const run = await client.actor('techforce.global/youtube-scraper').call({
  channelUrls: ['https://www.youtube.com/@mkbhd'],
  maxVideos: 10,
  scrapeComments: true,
  maxComments: 100,
  deliveryMode: 'none',
  proxyConfiguration: { useApifyProxy: true },
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();

// Three record types in one dataset — split them first.
const videos   = items.filter((i) => i.type === 'video');
const comments = items.filter((i) => i.type === 'comment');
const channels = items.filter((i) => i.type === 'channel');

// Comments join back to their video on videoId.
const byVideo = new Map(videos.map((v) => [v.videoId, { ...v, comments: [] as any[] }]));
for (const c of comments) byVideo.get(c.videoId)?.comments.push(c);

for (const v of byVideo.values()) {
  // commentCount is YouTube's number; v.comments.length is what we actually got.
  console.log(`${v.title}: ${v.comments.length}/${v.commentCount ?? '?'} comments scraped`);
}
console.log(channels[0]?.subscriberCount ?? 'subscriber count hidden');
```

#### cURL — synchronous (waits, returns the dataset)

```bash
curl -X POST \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
  -d '{
        "searchQueries": ["python tutorial 2024"],
        "maxVideos": 10,
        "scrapeComments": false,
        "deliveryMode": "none",
        "proxyConfiguration": { "useApifyProxy": true }
      }' \
  'https://api.apify.com/v2/acts/techforce.global~youtube-scraper/run-sync-get-dataset-items'
```

#### cURL — asynchronous with a spend cap (recommended for large runs)

```bash
## 1. Start with a hard spend limit
curl -X POST -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
  -d '{ "channelUrls": ["https://www.youtube.com/@handle"], "maxVideos": 200, "maxComments": 200,
        "proxyConfiguration": { "useApifyProxy": true } }' \
  'https://api.apify.com/v2/acts/techforce.global~youtube-scraper/runs?maxTotalChargeUsd=10'

## 2. Poll, then 3. fetch the dataset when SUCCEEDED
curl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' 'https://api.apify.com/v2/actor-runs/%3CrunId%3E'
curl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
  'https://api.apify.com/v2/datasets/%3CDATASET_ID%3E/items?clean=true&format=json'
```

`maxTotalChargeUsd` is the hard ceiling. **Always set it** when `maxVideos` × `maxComments` runs
into the thousands.

#### Metadata-only configuration — no personal data

```json
{
  "searchQueries": ["ai agents 2026"],
  "hashtags": ["#llm"],
  "maxVideos": 50,
  "scrapeComments": false,
  "maxComments": 0,
  "deliveryMode": "none",
  "proxyConfiguration": { "useApifyProxy": true }
}
```

Video rows only — no `author`, no `authorId`, no comment text. Faster, cheaper, and out of scope
for most of the [compliance](#-compliance--data-privacy) burden. **Start here.**

#### With MCP delivery — a digest into Slack

```json
{
  "searchQueries": ["apify web scraping"],
  "maxVideos": 15,
  "scrapeComments": false,
  "proxyConfiguration": { "useApifyProxy": true },
  "mcpConnector": "<your-authorized-slack-connector>",
  "mcpTool": "send_message",
  "deliveryMode": "summary",
  "mcpMessageTemplate": "Scraped {itemCount} YouTube results:\n\n{itemsText}",
  "mcpArguments": { "channel": "#youtube-watch", "text": "{message}" }
}
```

For competitor tracking, put a fixed `channelUrls` input on a
[schedule](https://docs.apify.com/platform/schedules) with comments off, and join runs on `videoId`
to watch `viewCount` curves over time.

***

### ⚠️ Autonomous Agent Error Handling & Resolution Matrix

**Read this first:** nothing about the *content* fails the run. Missing inputs, a blocked request,
an unextractable video and a comment thread that won't load are all logged while the run continues
to `SUCCEEDED`. An agent that checks run status alone will read an empty dataset as
success-with-data — **check the row count and the log**.

The `CONDITION` labels are documentation handles for observable states; this Actor writes no error
code into the dataset.

| Condition | What you observe | Cause | Deterministic agent action |
|---|---|---|---|
| `NO_INPUT_PROVIDED` | `SUCCEEDED`, empty dataset; log: `No input provided. Supply at least one of: …` | All four input arrays empty — **not** caught by validation | **Modify Input:** supply at least one array. |
| `BOT_CHECK_BLOCKED` | Empty/thin dataset; log: `[yt-dlp] Error extracting …`, sign-in-to-confirm errors | Proxy disabled or platform IP blocked | **Modify Input:** `{"useApifyProxy": true}` and retry. |
| `NO_PROXY_CONFIGURED` | Log: `No proxy configured — YouTube is likely to block requests…` | `proxyConfiguration` omitted or unusable | **Modify Input:** enable Apify Proxy before retrying anything else. |
| `CHANNEL_UNRESOLVED` | That channel contributes no rows; log: `[yt-dlp] Error extracting <url>` | Not a resolvable channel, or blocked | **Modify Input:** use the `@handle` or `/channel/ID` form. |
| `VIDEO_EXTRACT_FAILED` | A requested video URL produced no row | Private, deleted, region-blocked, or challenged | **Do not retry blindly.** Verify the URL opens signed-out. |
| `COMMENTS_FAILED` | Video rows present, no comment rows; log: `[comments] Failed for <videoId>` | Comments disabled on that video, or the fetch was blocked | **Expected, partial.** Retry once. |
| `TYPE_MISMATCH_IN_CONSUMER` | Consumer crashes reading `title` / `text` | One dataset, three shapes | **Fix consumer:** branch on `type`. |
| `SHORT_NOT_FLAGGED` | A Short has `isShort: false` | Flag needs a `/shorts/` page URL **and** ≤60s | **Fix expectation:** filter `duration <= 60`, or use the [Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper). |
| `NULL_LIKE_COUNT` | `likeCount` / `subscriberCount` null | Creator or channel hides the number | **Expected.** Null ≠ zero. |
| `COMMENT_COUNT_MISMATCH` | Fewer comment rows than `commentCount` | `maxComments` cap, or YouTube served fewer | **Expected.** Raise `maxComments` (max 1,000). |
| `PUBLISHED_AT_NOT_PARSEABLE` | `publishedAt` won't parse as a date | Relative text by design | **Fix expectation:** use `scrapedAt`. |
| `DUPLICATE_VIDEOS` | Same `videoId` twice, different `source` | Matched by two inputs; no dedup | **Expected.** Dedupe on `videoId`. |
| `ITEM_EXPLOSION` | Dataset far larger than expected | `maxVideos` per input × `maxComments` per video | **Modify Input:** lower both, or `scrapeComments: false`. |
| `FREE_PLAN_CAPPED` | 5 items; log: `Free plan detected - capping results to 5 item(s).` | Free plan — cap is **total**, not per input | **Upgrade the plan.** |
| `CONNECTOR_CALL_STORM` | Hundreds/thousands of connector calls | `perItem` sends one call per row, comments included | **Modify Input:** switch to `chunked` or `summary`. |
| `CONNECTOR_TOOL_NOT_FOUND` | Log: `Tool 'x' is not available on this connector. Available tools: …` | Wrong `mcpTool` name (or empty — logged separately) | **Modify Input:** copy an exact name from that log line. Dataset unaffected. |
| `CONNECTOR_UNREACHABLE` | Log: `Could not reach the connector: …` or `APIFY_MCP_PROXY_URL is not set` | Connector not authorised, or running locally | **Authorise it**, or `apify push` and run on the platform. Data already saved. |
| `AUTH_INVALID` | HTTP `401` | Missing or malformed `APIFY_TOKEN` | **Abort & prompt user** for a valid token. |
| `RATE_LIMITED` | HTTP `429` | Apify API rate / concurrency limits | **Retry:** exponential backoff (2s, 4s, 8s). |
| `SYNC_TIMEOUT` | HTTP `408` on the sync endpoint | Run exceeded the synchronous ceiling | **Reconfigure:** async `POST /runs` + polling. |

> **Provenance:** every log line, status and dataset condition above is taken from this Actor's own
> source and verified against it. The three HTTP rows describe the Apify **API transport layer**,
> not this Actor — confirm them against the [Apify API reference](https://docs.apify.com/api/v2)
> before wiring them into automated retry logic.

***

### 🗣️ Natural Language → Actor Mapping

| A user says… | Intent | Constructed Actor input |
|---|---|---|
| "Find YouTube videos about X" | Search | `{"searchQueries":["X"],"maxVideos":20}` |
| "Top videos for this hashtag" | Hashtag discovery | `{"hashtags":["#X"],"maxVideos":20}` |
| "What has this channel posted lately?" | Channel listing | `{"channelUrls":["<url>"],"maxVideos":25}` |
| "How many subscribers does this channel have?" | Channel stats | `{"channelUrls":["<url>"],"maxVideos":1,"scrapeChannelInfo":true}` |
| "Get the stats for these three videos" | Direct metadata | `{"videoUrls":["<a>","<b>","<c>"],"scrapeComments":false}` |
| "What are people saying in the comments?" | Comment mining | `{"videoUrls":["<url>"],"scrapeComments":true,"maxComments":500}` |
| "Sentiment on this topic across YouTube" | Social listening | `{"searchQueries":["<topic>"],"maxVideos":10,"maxComments":200}` |
| "Just the video data, no personal data" | Privacy-safe pull | `{"searchQueries":["<topic>"],"scrapeComments":false,"maxComments":0}` |
| "Compare two competitors' channels" | Competitive analysis | `{"channelUrls":["<A>","<B>"],"maxVideos":25,"scrapeComments":false}` |
| "Which of their videos performed best?" | Ranking | Channel input, then sort on `viewCount` excluding nulls |
| "Track this channel weekly" | Scheduled monitor | Fixed input on a schedule; join runs on `videoId` |
| "Which of these are Shorts?" | Format filter | Scrape, then filter `duration <= 60` — `isShort` under-reports |
| "Put these results in Notion" | Scrape + deliver | Above, plus `mcpConnector`, `mcpTool`, `deliveryMode: "chunked"` |
| "It returned nothing" | Empty result | Check an input array was supplied, and that Apify Proxy is on |
| "What did they actually say in the video?" | Transcript | ➡️ [Transcript Scraper](https://apify.com/techforce.global/youtube-transcript-scraper) |
| "Transcripts for their whole channel" | Corpus | ➡️ [Full Channel Transcripts Scraper](https://apify.com/techforce.global/youtube-full-channel-transcripts-scraper) |
| "List all their Shorts / download them" | Short-form | ➡️ [Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper) |

**Should NOT route here:** **transcripts or captions** (→ [Transcript](https://apify.com/techforce.global/youtube-transcript-scraper) / [Full Channel](https://apify.com/techforce.global/youtube-full-channel-transcripts-scraper)) ·
**a channel's Shorts catalogue or video files** (→ [Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper)) ·
**creator analytics** — watch time, retention, revenue · **viewer demographics** · **private,
unlisted or members-only content** · posting comments, uploading, or any write action ·
non-YouTube video platforms (Vimeo, TikTok, Twitch).

***

### 🧭 Agent Execution & Routing Logic

```text
Does the user want YouTube NUMBERS or COMMENTS (not words)?
├── NO → one of the siblings.
│        one video's transcript   → YouTube Transcript Scraper
│        a channel's transcripts  → Full Channel Transcripts Scraper
│        a channel's Shorts/files → YouTube Shorts Scraper
│        watch time / revenue     → creator's own account only
│
└── YES
    │
    ├── Which input do they have? (mix freely; at least ONE is mandatory at
    │   runtime even though the schema marks none required)
    │     ├── a topic          → searchQueries
    │     ├── a hashtag        → hashtags        (run as a "#tag" search)
    │     ├── a creator        → channelUrls     (adds a channel row)
    │     └── specific videos  → videoUrls       (most predictable item count)
    │
    ├── ALWAYS keep proxyConfiguration enabled (useApifyProxy: true)
    │
    ├── Do they want COMMENTS?
    │     ├── no  → scrapeComments: false, maxComments: 0
    │     │          → smaller, faster, and NO personal data
    │     └── yes → maxComments is PER VIDEO (0-1000)
    │           ├── personal data: author, authorId, text → see Compliance
    │           └── multiply: videos × maxComments = row count
    │
    ├── How many videos? maxVideos is PER search / hashtag / channel (1-500)
    │     ├── a probe → 3      ├── analysis → 20-50      └── deep → + maxTotalChargeUsd
    │
    ├── What is the actual question?
    │     ├── best performers → sort viewCount, EXCLUDING nulls
    │     ├── like-rate       → likeCount / viewCount, both non-null only
    │     ├── Shorts vs long  → duration <= 60  (NOT isShort)
    │     ├── audience mood   → comment text + likes, join on videoId
    │     └── growth over time→ SCHEDULE it; join on videoId
    │
    └── Should results land in another app?
          ├── yes → mcpConnector + mcpTool + {message} in mcpArguments
          │           one digest → summary · long list → chunked
          │           per record → perItem (ONE CALL PER ROW, comments included)
          └── no  → deliveryMode "none", or leave mcpConnector empty

[Execute Actor]
    ├──► SUCCEEDED with rows → split by `type`, then consume
    ├──► SUCCEEDED, 0 rows   → READ THE LOG: no input / blocked / unresolvable
    └──► exactly 5 rows      → free plan cap, not a data pull
```

***

### 💰 Cost Control & Pricing Transparency

**Billing model: usage-based.** This Actor bills for **platform usage** — the compute time and
resources a run consumes — not per row. Current rates are on the
[Actor's Apify Store page](https://apify.com/techforce.global/youtube-scraper); the Console shows
the live total mid-run.

| Choice | Effect on cost |
|---|---|
| `maxVideos` | **The main driver**, and it applies **per** search, hashtag and channel. |
| Number of inputs | Multiplies `maxVideos` — three searches at 20 is 60 videos, not 20. |
| `scrapeComments` | **The second-biggest lever:** one extra fetch per video, and comment rows dominate dataset size. |
| `maxComments` | Up to 1,000 rows per video. 20 videos × 500 = 10,000 rows. |
| Channel inputs | Each video in a channel needs its own full metadata request. |
| `scrapeChannelInfo` | Negligible — one row per channel from data already fetched. |
| Being blocked | Costs run time and returns nothing. **A disabled proxy is the most expensive setting on this page.** |

#### Worked examples — relative run shape

| Scenario | Videos | Comment rows | Total rows | Notes |
| :--- | :--- | :--- | :--- | :--- |
| Probe — 1 search, `maxVideos: 3`, no comments | 3 | 0 | 3 | **Baseline.** Confirms proxy and input work. |
| Topic scan — 1 search, 20 videos, no comments | 20 | 0 | 20 | The cheap research shape. |
| Same, with `maxComments: 50` | 20 | ≤ 1,000 | ≤ 1,020 | Comments dominate immediately. |
| Two searches, 20 each, `maxComments: 50` | 40 | ≤ 2,000 | ≤ 2,040 | The execution-rules example. |
| Channel audit — 1 channel, 25 videos, no comments | 25 | 0 | 26 | Plus one `channel` row. |
| Comment deep-dive — 2 video URLs, `maxComments: 500` | 2 | ≤ 1,000 | ≤ 1,002 | Most predictable comment-heavy shape. |
| Broad sweep — 5 searches × 100 videos × 200 comments | 500 | ≤ 100,000 | ≤ 100,500 | **Set `maxTotalChargeUsd` before running this.** |

**Keeping runs predictable:**

1. **First run: one input, `maxVideos: 3`, `scrapeComments: false`.**
2. **Decide the comment question up front** — it's the difference between a 20-row dataset and a
   1,020-row one, and between being in and out of scope for data-protection obligations.
3. **Prefer `videoUrls` when you can** — a search's item count is a prediction; a list of video
   URLs is arithmetic.
4. **Always set `maxTotalChargeUsd`** on runs whose row count reaches the thousands.
5. **Cache by `videoId`** so scheduled re-runs don't re-pay for unchanged videos.
6. **Free Apify plans get 5 items per run** — a connectivity check, not a data pull.

***

### 🛠️ Troubleshooting

Most problems are **missing input, the proxy, or the three-record-type shape** — not scraper bugs.

| # | Symptom you see | Most likely cause | Fix |
| :-- | :--- | :--- | :--- |
| 1 | `SUCCEEDED` but the dataset is **empty** | No input array supplied — the schema doesn't enforce it | Read the log for `No input provided…`, then supply `searchQueries`, `hashtags`, `channelUrls` or `videoUrls`. |
| 2 | Empty or thin results despite valid input | Bot check — YouTube blocks Apify's platform IPs | Confirm `proxyConfiguration` is `{"useApifyProxy": true}`; the log warns when no proxy is configured. |
| 3 | Consumer crashes on a missing field | One dataset holds three shapes | Branch on `type` before reading any field. |
| 4 | A Short shows `isShort: false` | Flag needs a `/shorts/` page URL **and** ≤60s; search/channel inputs don't provide that | Filter `duration <= 60`, or use the [Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper). |
| 5 | `likeCount` / `subscriberCount` null | Creator or channel hides the number | Expected. Null ≠ zero — exclude from averages. |
| 6 | Far fewer comment rows than `commentCount` | `maxComments` cap, or YouTube served fewer | Raise `maxComments` (max 1,000). Completeness isn't guaranteed. |
| 7 | No comment rows at all | `scrapeComments: false`, `maxComments: 0`, or comments disabled | Check the input, then the log for `[comments] Failed for <videoId>`. |
| 8 | `publishedAt` won't parse as a date | It's YouTube's relative text (`"3 weeks ago"`) | Use `scrapedAt` (ISO-8601 UTC) for timing. |
| 9 | The description looks cut off | Truncated at 2,000 characters by design | Fetch the page yourself if you need the full text. |
| 10 | The same video appears twice | Matched two inputs; `source` differs | No dedup by design — dedupe on `videoId`. |
| 11 | Dataset far bigger than expected | `maxVideos` is per input; `maxComments` is per video | Do the multiplication, lower the caps, or turn comments off. |
| 12 | The run takes far longer than expected | Comments add a fetch per video; channels add a full request per video | Run metadata-only first, then target specific videos. |
| 13 | Exactly 5 rows came back | Free plan cap — **total**, not per input | Upgrade the plan; the log says `Free plan detected…`. |
| 14 | A free-plan run returned slightly more than 5 rows | The cap counts push batches, and a `videoUrls` entry pushes its video and comments together | Expected on free plans; paid runs are uncapped. |
| 15 | One channel produced nothing in a multi-channel run | That channel's extraction failed — `[yt-dlp] Error extracting <url>` | Re-run it alone with the `@handle` form. |
| 16 | A specific video URL produced no row | Private, deleted, region-blocked, or challenged | Open it signed-out in a browser to confirm. |
| 17 | Hundreds of connector calls fired | `perItem` sends one call per row, comments included | Switch to `chunked` or `summary`. |
| 18 | Connector messages have blank fields | `perItem` template referenced a field that record type lacks | Branch the template on `{type}`, or use `summary`. |
| 19 | Slack / Notion received nothing | Delivery needs **both** `mcpConnector` and `mcpTool` | Set both. Every dataset row is still saved. |
| 20 | Delivery fired when you only wanted a dataset | `deliveryMode` defaults to `summary`, **not** `none` | Set `deliveryMode: "none"` or leave `mcpConnector` empty. |
| 21 | Log: *APIFY\_MCP\_PROXY\_URL is not set* | Ran locally; connectors only resolve on the platform | `apify push` and run on the platform. |
| 22 | `HTTP 401` / `403` | Missing, expired or malformed `APIFY_TOKEN` | Regenerate in **Console → Settings → API & Integrations**; send as `Authorization: Bearer <token>`. |
| 23 | `HTTP 429` / `408` | Apify rate limits / the sync endpoint's time ceiling | Back off (2s → 4s → 8s), or switch to async `POST /runs` + polling. |

#### Diagnostic checklist before opening an issue

1. **Confirm an input array is non-empty** — the most common cause of an empty run.
2. **Confirm Apify Proxy is enabled** and the log has no no-proxy warning.
3. **Baseline run:** one `searchQueries` entry, `maxVideos: 3`, `scrapeComments: false`.
4. **Count rows by `type`** before concluding anything is missing.
5. **Open the failing URL signed-out** — private, deleted and region-blocked videos look identical
   to a scraper.
6. **Read the log** — per-item failures appear as `[yt-dlp] Error extracting …` and
   `[comments] Failed for …`.

If the issue survives all six, open an **Issues** ticket (or email support) with the **run ID**, the
**input you used**, and what you expected.

***

### ❓ FAQ

**Do I need a YouTube Data API key or a Google Cloud project?**

No. The Actor reads publicly available YouTube pages. You need an **Apify** account and API token; you need no Google credentials whatsoever.

**Is this the official YouTube API?**

No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by YouTube or Google.

**What's the real advantage over the Data API?**

The quota. The official API gives you 10,000 units a day and a single search costs 100 of them — roughly 100 searches before you're locked out until midnight Pacific. This Actor has no quota, no Google Cloud project and no OAuth flow. The trade-off: the API is the sanctioned route with a versioned contract, while this Actor depends on YouTube's page structure.

**Do I need to configure proxies?**

Apify Proxy is on by default and you should leave it on. YouTube blocks the platform's shared IPs with a "Sign in to confirm you're not a bot" wall, which shows up as an empty or thin dataset. A fresh IP is rotated in per request.

**Can I plug it into Claude, Cursor, or a LangChain agent?**

Yes — it is a native MCP tool: `claude mcp add --transport http apify "https://mcp.apify.com/?tools=techforce.global/youtube-scraper"`

**Which inputs can I combine in one run?**

All four — `searchQueries`, `hashtags`, `channelUrls` and `videoUrls` — freely mixed. They're processed in that order, and every video row carries a `source` field so you can tell which input produced it.

**How do hashtags work?**

Each hashtag is run as a search for `#tag` (the `#` is added if you leave it off). That's the honest description: it is a search, not a separate hashtag-page crawl. The official API has no hashtag concept at all.

**How many items will my run produce?**

Multiply before you run. `maxVideos` is **per** search, hashtag and channel; `maxComments` is **per video**. Two searches at `maxVideos: 20` with `maxComments: 50` is up to 40 videos + 2,000 comments = 2,040 rows.

**How do I get video metadata without collecting personal data?**

Set `scrapeComments: false` (or `maxComments: 0`). You get `video` and `channel` rows only — no author names, no author IDs, no comment text.

**Why do my rows have different shapes, and how do I join them?**

One dataset holds three record types — `video`, `channel` and `comment` — discriminated by the `type` field. Always branch on `type` before reading fields; a consumer that assumes every row has `title` breaks on the first comment. Comment rows carry `videoId`, which joins them back to their video.

**Why is `likeCount` null?**

The creator hid the like count; `subscriberCount` behaves the same way on channels. **Null is not zero** — exclude those rows from averages rather than coercing them.

**Are the dates ISO-8601?**

`uploadDate` is normalised to `YYYY-MM-DD` and `scrapedAt` is full ISO-8601 UTC. A comment's `publishedAt` is **relative text** such as `"3 weeks ago"`, because that is all YouTube publishes — use `scrapedAt` for anything time-sensitive.

**Why is `isShort` false for a video I know is a Short?**

Because the flag requires the video's own page URL to be a `/shorts/` URL **and** the duration to be 60 seconds or less. Videos reached through search, a channel listing or a `/watch?v=` URL — nearly everything this Actor returns — normally report `false`. Filter on `duration <= 60`, or use the [YouTube Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper), which reads the Shorts tab directly.

**Why did I get fewer comments than `commentCount` says?**

`commentCount` is the number YouTube reports for the video. The rows you receive are capped by `maxComments` (max 1,000) and by what YouTube actually served.

**Can the same video appear twice?**

Yes. There is no deduplication: a video matched by two searches, or by a search and a channel, is written twice with a different `source`. Dedupe on `videoId` downstream.

**Can I get transcripts, or download the video files?**

Neither. For transcripts use the [YouTube Transcript Scraper](https://apify.com/techforce.global/youtube-transcript-scraper) (one video) or the [Full Channel Transcripts Scraper](https://apify.com/techforce.global/youtube-full-channel-transcripts-scraper). For a channel's Shorts as MP4 or audio, use the [YouTube Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper). This Actor returns metadata and thumbnail URLs only.

**Which delivery mode should I use, and does it work locally?**

`summary` for a single digest, `chunked` for large result sets into services with request caps (Notion). Avoid `perItem` unless you really want one record each: delivery reads the whole dataset, so it issues one call per row — comments included, which can be thousands. Delivery only works on the Apify platform; locally you'll see `APIFY_MCP_PROXY_URL is not set` and the dataset is saved as normal.

**Do comments contain personal data?**

Yes. `author`, `authorId` and `text` identify real people, and `authorId` is a stable identifier for an individual. `scrapeComments` defaults to `true`, so your first run collects them unless you turn it off. Read [Compliance & Data Privacy](#-compliance--data-privacy) before you store or reuse that data.

**What does it cost, and what do I get on the free plan?**

Usage-based: you pay for the platform usage a run consumes, not per row. The drivers are `maxVideos`, the number of inputs, and whether comments are on. Free Apify plans return 5 items **total** per run — a connectivity check, not a data pull. Pass `maxTotalChargeUsd` on the run endpoint for a hard per-run ceiling.

***

### 🔍 Companion machine-readable files

| File | What it defines |
|---|---|
| [`.actor/input_schema.json`](.actor/input_schema.json) | Every input, its type, default, bounds and the delivery enum |
| [`.actor/dataset_schema.json`](.actor/dataset_schema.json) | Every field across all three record types, and the four Console views |
| [`.actor/output_schema.json`](.actor/output_schema.json) | Where the run's output is exposed |
| [`.actor/actor.json`](.actor/actor.json) | Actor identity and storage wiring |

> **If this page and the schema files disagree, the schema files win.** They are what the platform
> validates against at run time; this page is documentation and can lag a release.

***

### 🔗 Related Actors by Techforce Global

This Actor is the **numbers-and-comments specialist**: discovery, metadata and audience reaction.

| Actor | Best for | Why pick it over this one |
| :--- | :--- | :--- |
| [YouTube Transcript Scraper](https://apify.com/techforce.global/youtube-transcript-scraper) | **One video's** timestamped transcript, 13 languages with auto-translation | You want what was said in a specific video, not how it performed. |
| [YouTube Full Channel Transcripts Scraper](https://apify.com/techforce.global/youtube-full-channel-transcripts-scraper) | **Transcripts for every video** on a channel | You're building a RAG corpus or searchable archive of a creator's words. |
| [YouTube Shorts Scraper](https://apify.com/techforce.global/youtube-shorts-scraper) | A channel's **Shorts** with engagement, optional MP4/audio download | You need the Shorts tab specifically — this Actor's `isShort` under-reports — or the media files. |
| [Advanced Website Crawling Actor](https://apify.com/techforce.global/advanced-website-crawling-actor) | Whole-site text, Markdown or HTML | You want a creator's or competitor's site in the same corpus as their video data. |
| [Pinterest Scraper](https://apify.com/techforce.global/pinterest-scraper) | Pins from searches, boards, profiles | You're comparing the same content strategy on another visual platform. |

**Pipeline patterns:** **topic landscape** — several `searchQueries`, comments off, rank by
`viewCount`, then send the winners to the Full Channel Transcripts Scraper for a content-gap
analysis · **audience sentiment** — `videoUrls` with `maxComments: 500`, comment text into your LLM,
digest to Slack via MCP · **competitor tracking** — `channelUrls` on a schedule, comments off, join
runs on `videoId` to watch view curves and posting cadence.

> Browse all Actors by Techforce Global at **[scraper.techforce.global](https://scraper.techforce.global/)**.

***

### 🔐 Compliance & Data Privacy

**Trademark & affiliation.** This is an independent Actor. It is **not** affiliated with, endorsed
by, or sponsored by YouTube or Google. YouTube™ is a trademark of Google LLC; all trademarks are
the property of their respective owners.

- **Public content only.** Everything returned is visible to any signed-out visitor. No private,
  unlisted or members-only content, no login, no stored credentials.

- **⚠️ Comments are personal data — and they're on by default.** `author`, `authorId` and `text`
  identify real people, and `authorId` is a **stable identifier** that lets you track one person
  across videos. Under **GDPR / UK GDPR** and **CCPA/CPRA**, collecting and storing them is
  processing personal data, and the obligations sit with you, not with the Actor.

- **The cheapest compliance measure is a single field.** If your question is about *videos* rather
  than *people*, set `scrapeComments: false`. You keep every video and channel metric and remove
  the personal data entirely.

- **If you do collect comments:** have a lawful basis, keep retention short, restrict access, and
  don't re-publish individuals' comments out of context. Comment text is unfiltered public speech —
  it can contain special-category content (health, politics, religion, sexuality) and abuse, and
  you cannot know which threads contain it until after retrieval.

- **Aggregate rather than store where you can.** Sentiment scores, theme counts and engagement
  ratios rarely require keeping the raw `author` / `authorId` columns. Drop them at ingest.

- **Public does not mean consented.** Someone commenting under a video did not agree to be
  profiled, ranked, or added to a dataset that follows them across channels. Treat `authorId`
  linkage as the sensitive operation it is.

- **Video content and metadata are the creators' work.** Internal research, analysis and reporting
  sit on far safer ground than republishing scraped descriptions, thumbnails or metrics as your
  own content.

- **Counts are a snapshot.** `viewCount`, `likeCount` and `subscriberCount` are true as of
  `scrapedAt`, and hidden values come back `null`, not `0`. Publishing rankings or like-rates
  without saying when you captured them — or while silently treating nulls as zeros — produces
  numbers that look authoritative and aren't.

- **YouTube's Terms of Service prohibit automated access.** **This Actor does not remove that
  risk — it is a risk you accept by running it.** The
  [official Data API](https://developers.google.com/youtube/v3) is the sanctioned route; its quota
  is the reason this Actor exists.

- **Attribution over mirroring.** `url` and `channelUrl` are on every video row. Link back and
  credit the creator rather than presenting their content or their numbers as your own.

***

### 🆘 Support & Custom Pipeline Engineering

Need automated multi-city scheduled feeds, webhook enrichment, or enterprise data warehouse syncs?

- **Email**: bhavin.shah@techforceglobal.com
- **Custom Enterprise Integrations**: [Book a 15-Minute Technical Consultation](https://calendly.com/techforce-global/intro-meeting)
- **Maintained by**: [Techforce Global](https://www.techforceglobal.com) — Specialists in High-Performance Web Scrapers and Agentic Workflows.

***

<sub>Built and maintained by **Techforce Global**. Scrapes public YouTube video, channel and
comment data only — no logins, no private content, no transcripts, no video files, no creator
analytics. Comment rows contain personal data (`author`, `authorId`, `text`) and comment scraping
is **on by default**; set `scrapeComments: false` if you don't need it. Engagement counts are true
only as of `scrapedAt`, and nulls mean hidden, not zero. You are responsible for complying with
YouTube's Terms of Service and with the data-protection law of your jurisdiction.</sub>

***

### 🏷️ Structured data for search & AI discovery

`SoftwareApplication` describes the Actor; `FAQPage` wraps the FAQ above so those answers are
eligible for rich results, matching it question-for-question.

```json
{
  "@context": "https://schema.org/",
  "@type": "SoftwareApplication",
  "name": "YouTube Scraper – Videos, Channels & Comments",
  "alternateName": "YouTube Data API Alternative",
  "applicationCategory": "BusinessApplication",
  "applicationSubCategory": "Web Scraping / Video & Audience Analytics",
  "operatingSystem": "Cloud (Apify platform)",
  "description": "Scrapes YouTube videos, channels and comments from search terms, hashtags, channel URLs or video URLs without a YouTube Data API key, Google Cloud project, or daily quota. Returns three record types in one dataset — video metadata with view, like and comment counts; channel stats including subscribers; and comment threads with author, likes and reply flags — each discriminated by a type field. Public content only, with comment scraping switchable off in one field for callers who do not want personal data. Apify Proxy rotates a fresh IP per request, and results can be delivered into Notion, Slack, Linear, Airtable or any MCP connector.",
  "url": "https://apify.com/techforce.global/youtube-scraper",
  "author": { "@type": "Organization", "name": "Techforce Global", "url": "https://www.techforceglobal.com/" },
  "provider": { "@type": "Organization", "name": "Apify", "url": "https://apify.com/" },
  "offers": {
    "@type": "Offer",
    "priceCurrency": "USD",
    "category": "Pay per platform usage",
    "description": "Usage-based: billed for the compute time and resources a run consumes, not per row. Current rates are shown on the Actor's Apify Store page.",
    "url": "https://apify.com/techforce.global/youtube-scraper"
  },
  "featureList": [
    "Four input types in one run: search terms, hashtags, channel URLs and video URLs",
    "No YouTube Data API key, Google Cloud project, OAuth or daily quota",
    "Three record types in one dataset, discriminated by a type field",
    "Video metadata with view, like and comment counts, duration and tags",
    "Channel stats including subscriber and video counts",
    "Comment threads with author, likes, reply count and reply flag, up to 1,000 per video",
    "Comment scraping switchable off in one field for a personal-data-free dataset",
    "source field records which input produced each video",
    "uploadDate normalised to YYYY-MM-DD and scrapedAt in ISO-8601 UTC",
    "Apify Proxy with a fresh IP rotated in per request",
    "Per-item failures logged and skipped so one bad video never ends the run",
    "Optional MCP delivery to Notion, Slack, Linear, Airtable and more",
    "Four Console dataset views covering videos, comments and channels"
  ],
  "isAccessibleForFree": false,
  "softwareRequirements": "Apify account and API token",
  "keywords": "YouTube Data API alternative, YouTube scraper, YouTube comment scraper, video metadata extraction, channel statistics, hashtag search, audience insight, social listening, content research, MCP connector"
}
```

```json
{
  "@context": "https://schema.org/",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Do I need a YouTube Data API key or a Google Cloud project?",
      "acceptedAnswer": { "@type": "Answer", "text": "No. The Actor reads publicly available YouTube pages. You need an Apify account and API token; you need no Google credentials whatsoever." }
    },
    {
      "@type": "Question",
      "name": "Is this the official YouTube API?",
      "acceptedAnswer": { "@type": "Answer", "text": "No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by YouTube or Google." }
    },
    {
      "@type": "Question",
      "name": "What's the real advantage over the Data API?",
      "acceptedAnswer": { "@type": "Answer", "text": "The quota. The official API gives you 10,000 units a day and a single search costs 100 of them — roughly 100 searches before you're locked out until midnight Pacific. This Actor has no quota, no Google Cloud project and no OAuth flow. The trade-off: the API is the sanctioned route with a versioned contract, while this Actor depends on YouTube's page structure." }
    },
    {
      "@type": "Question",
      "name": "Do I need to configure proxies?",
      "acceptedAnswer": { "@type": "Answer", "text": "Apify Proxy is on by default and you should leave it on. YouTube blocks the platform's shared IPs with a \"Sign in to confirm you're not a bot\" wall, which shows up as an empty or thin dataset. A fresh IP is rotated in per request." }
    },
    {
      "@type": "Question",
      "name": "Can I plug it into Claude, Cursor, or a LangChain agent?",
      "acceptedAnswer": { "@type": "Answer", "text": "Yes — it is a native MCP tool: claude mcp add --transport http apify \"https://mcp.apify.com/?tools=techforce.global/youtube-scraper\"" }
    },
    {
      "@type": "Question",
      "name": "Which inputs can I combine in one run?",
      "acceptedAnswer": { "@type": "Answer", "text": "All four — searchQueries, hashtags, channelUrls and videoUrls — freely mixed. They're processed in that order, and every video row carries a source field so you can tell which input produced it." }
    },
    {
      "@type": "Question",
      "name": "How do hashtags work?",
      "acceptedAnswer": { "@type": "Answer", "text": "Each hashtag is run as a search for #tag (the # is added if you leave it off). That's the honest description: it is a search, not a separate hashtag-page crawl. The official API has no hashtag concept at all." }
    },
    {
      "@type": "Question",
      "name": "How many items will my run produce?",
      "acceptedAnswer": { "@type": "Answer", "text": "Multiply before you run. maxVideos is per search, hashtag and channel; maxComments is per video. Two searches at maxVideos: 20 with maxComments: 50 is up to 40 videos + 2,000 comments = 2,040 rows." }
    },
    {
      "@type": "Question",
      "name": "How do I get video metadata without collecting personal data?",
      "acceptedAnswer": { "@type": "Answer", "text": "Set scrapeComments: false (or maxComments: 0). You get video and channel rows only — no author names, no author IDs, no comment text." }
    },
    {
      "@type": "Question",
      "name": "Why do my rows have different shapes, and how do I join them?",
      "acceptedAnswer": { "@type": "Answer", "text": "One dataset holds three record types — video, channel and comment — discriminated by the type field. Always branch on type before reading fields; a consumer that assumes every row has title breaks on the first comment. Comment rows carry videoId, which joins them back to their video." }
    },
    {
      "@type": "Question",
      "name": "Why is likeCount null?",
      "acceptedAnswer": { "@type": "Answer", "text": "The creator hid the like count; subscriberCount behaves the same way on channels. Null is not zero — exclude those rows from averages rather than coercing them." }
    },
    {
      "@type": "Question",
      "name": "Are the dates ISO-8601?",
      "acceptedAnswer": { "@type": "Answer", "text": "uploadDate is normalised to YYYY-MM-DD and scrapedAt is full ISO-8601 UTC. A comment's publishedAt is relative text such as \"3 weeks ago\", because that is all YouTube publishes — use scrapedAt for anything time-sensitive." }
    },
    {
      "@type": "Question",
      "name": "Why is isShort false for a video I know is a Short?",
      "acceptedAnswer": { "@type": "Answer", "text": "Because the flag requires the video's own page URL to be a /shorts/ URL and the duration to be 60 seconds or less. Videos reached through search, a channel listing or a /watch?v= URL — nearly everything this Actor returns — normally report false. Filter on duration <= 60, or use the YouTube Shorts Scraper, which reads the Shorts tab directly." }
    },
    {
      "@type": "Question",
      "name": "Why did I get fewer comments than commentCount says?",
      "acceptedAnswer": { "@type": "Answer", "text": "commentCount is the number YouTube reports for the video. The rows you receive are capped by maxComments (max 1,000) and by what YouTube actually served." }
    },
    {
      "@type": "Question",
      "name": "Can the same video appear twice?",
      "acceptedAnswer": { "@type": "Answer", "text": "Yes. There is no deduplication: a video matched by two searches, or by a search and a channel, is written twice with a different source. Dedupe on videoId downstream." }
    },
    {
      "@type": "Question",
      "name": "Can I get transcripts, or download the video files?",
      "acceptedAnswer": { "@type": "Answer", "text": "Neither. For transcripts use the YouTube Transcript Scraper (one video) or the Full Channel Transcripts Scraper. For a channel's Shorts as MP4 or audio, use the YouTube Shorts Scraper. This Actor returns metadata and thumbnail URLs only." }
    },
    {
      "@type": "Question",
      "name": "Which delivery mode should I use, and does it work locally?",
      "acceptedAnswer": { "@type": "Answer", "text": "summary for a single digest, chunked for large result sets into services with request caps (Notion). Avoid perItem unless you really want one record each: delivery reads the whole dataset, so it issues one call per row — comments included, which can be thousands. Delivery only works on the Apify platform; locally you'll see APIFY_MCP_PROXY_URL is not set and the dataset is saved as normal." }
    },
    {
      "@type": "Question",
      "name": "Do comments contain personal data?",
      "acceptedAnswer": { "@type": "Answer", "text": "Yes. author, authorId and text identify real people, and authorId is a stable identifier for an individual. scrapeComments defaults to true, so your first run collects them unless you turn it off. Read the Compliance & Data Privacy section before you store or reuse that data." }
    },
    {
      "@type": "Question",
      "name": "What does it cost, and what do I get on the free plan?",
      "acceptedAnswer": { "@type": "Answer", "text": "Usage-based: you pay for the platform usage a run consumes, not per row. The drivers are maxVideos, the number of inputs, and whether comments are on. Free Apify plans return 5 items total per run — a connectivity check, not a data pull. Pass maxTotalChargeUsd on the run endpoint for a hard per-run ceiling." }
    }
  ]
}
```

# Actor input Schema

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

List of search terms to look up on YouTube.

## `channelUrls` (type: `array`):

YouTube channel URLs to scrape (e.g. https://www.youtube.com/@mkbhd).

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

Direct YouTube video URLs to scrape.

## `hashtags` (type: `array`):

YouTube hashtags to search (with or without the # symbol).

## `maxVideos` (type: `integer`):

Maximum number of videos to scrape per search query, channel, or hashtag.

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

Maximum number of comments to scrape per video.

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

Enable comment scraping for each video.

## `scrapeChannelInfo` (type: `boolean`):

Include channel-level metadata (subscribers, description, etc.) when scraping channels.

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

YouTube blocks direct requests from Apify's platform IPs with a 'Sign in to confirm you're not a bot' error. Apify Proxy is required for reliable scraping — a fresh IP is rotated in for every request.

## `mcpConnector` (type: `string`):

Optionally deliver scraped videos, channels, and comments into a connector you have authorized — Notion, Slack, Linear, Airtable, Jira, GitHub, or any MCP-compatible connector. Leave empty to only save results to the dataset.

## `deliveryMode` (type: `string`):

How to deliver to the connector: 'summary' (one call covering every scraped result), 'chunked' (split many results across a few calls/pages so services like Notion never time out), 'perItem' (one call per individual video/channel/comment), or 'none' (save to dataset only).

## `mcpTool` (type: `string`):

Name of the tool to call on the connector (e.g. 'create\_page' for Notion, 'send\_message' for Slack, 'create\_issue' for Jira/GitHub). If unsure, run once with a connector selected — the log lists the connector's available tools.

## `mcpArguments` (type: `object`):

Arguments passed to the connector tool. String values support {placeholders}. In 'summary'/'chunked' mode: {itemCount}, {itemsText} (formatted list of videos/channels/comments), {message} (the rendered template below); 'chunked' also gives {part}/{partCount} (1-based part number and total — put these in a page title, e.g. 'YouTube results (part {part}/{partCount})'). In 'perItem' mode: {type} ('video'/'channel'/'comment') plus whichever of these fields the record has — video: {videoId}, {title}, {description}, {channelId}, {channelName}, {channelUrl}, {viewCount}, {likeCount}, {commentCount}, {duration}, {durationStr}, {thumbnail}, {isLive}, {isShort}, {tags}, {categories}, {language}, {uploadDate}, {url}; channel: {channelId}, {channelName}, {channelUrl}, {description}, {subscriberCount}, {videoCount}, {thumbnail}, {country}; comment: {videoId}, {commentId}, {text}, {author}, {authorId}, {likes}, {replyCount}, {isReply}, {publishedAt}; always available: {scrapedAt}, {message}. Example for Slack: {"channel": "#youtube", "text": "{message}"}.

## `mcpMessageTemplate` (type: `string`):

Optional template rendered and exposed as the {message} placeholder in the tool arguments. Summary/chunked example: 'Scraped {itemCount} YouTube results:\n\n{itemsText}'. Per-item example for a video: '{title} — {url} ({viewCount} views, {likeCount} likes)'.

## Actor input object example

```json
{
  "searchQueries": [
    "python tutorial 2024",
    "apify web scraping"
  ],
  "channelUrls": [
    "https://www.youtube.com/@mkbhd"
  ],
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "hashtags": [
    "#python",
    "#webdev"
  ],
  "maxVideos": 20,
  "maxComments": 50,
  "scrapeComments": true,
  "scrapeChannelInfo": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "deliveryMode": "summary",
  "mcpTool": "",
  "mcpArguments": {},
  "mcpMessageTemplate": ""
}
```

# Actor output Schema

## `results` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("techforce.global/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 = {}

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

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

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

```

## CLI example

```bash
echo '{}' |
apify call techforce.global/youtube-scraper --silent --output-dataset

```

## MCP server setup

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

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/GuJpUvbpV33NXQhrD/builds/6SbNzVfrdDHks7zbh/openapi.json
