# YouTube Scraper: Search, Channels, Playlists & Videos (`arman-bd/youtube-scraper`) Actor

Scrape YouTube search results, channels, playlists and individual videos in one run. Title, views, likes, duration, publish date, description, tags, channel stats and thumbnails. No API key, no daily quota, no OAuth — unlike the official Data API.

- **URL**: https://apify.com/arman-bd/youtube-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Videos, Social media, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 video scrapeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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 Scraper: Search, Channels, Playlists & Videos

![YouTube Scraper: Search, channels, playlists and videos in one run, exact view counts, duration, dates and tags](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/youtube-scraper.jpg)

**YouTube Scraper** takes search terms, channel handles, playlist URLs and video URLs in the same run and returns one record shape for all of them: title, exact view count, exact like count, duration, publish date, full description, tags, channel stats and thumbnails.

Every record carries `type`, `id` and `url`, so a search result, a channel's upload history and a one-off video lookup all land in the same table. **No API key, no daily quota, no OAuth, no proxy setup and no browser.**

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/youtube-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/youtube-scraper.md
```

### What you get

One record per video, short, channel or playlist. `type` tells them apart; every key exists on every record, and `null` means YouTube does not publish that field for that item.

| Output field | Meaning |
|---|---|
| `type` | `video`, `short`, `channel` or `playlist` |
| `id` | Video ID, channel ID or playlist ID. Your join key |
| `url` | Canonical YouTube link, built from the ID |
| `title` | Video, channel or playlist title |
| `input` | The search term or URL that produced this row |
| `position` | Rank within that input |
| `description` | Full video description, the channel's About text, or the playlist blurb |
| `viewCount` | **Exact** when video details are on. `viewCountIsExact` tells you which you got |
| `viewCountIsExact` | `true` for a precise figure, `false` for one parsed from a rounded label |
| `likeCount` | Exact like count. `null` when the creator has hidden likes, never `0` |
| `durationSeconds`, `duration` | `213` and `3:33` |
| `publishedAt` | ISO 8601 publish time. Present when video details are on |
| `publishedText` | YouTube's own relative string, e.g. `4 years ago` |
| `isLive`, `wasLive` | Currently streaming, and ever streamed |
| `tags` | The uploader's own keywords. Video details only |
| `categoryName` | `Music`, `Education`, and so on. Video details only |
| `thumbnailUrl` | Largest thumbnail offered |
| `channelId`, `channelName`, `channelHandle`, `channelUrl` | Who published it |
| `subscriberCount` | **Approximate.** YouTube publishes only a rounded figure |
| `badges` | `4K`, `CC`, `LIVE` |
| `videoCount` | Uploads on a channel, or entries in a playlist |
| `totalViewCount` | Lifetime views for a channel, or for a playlist |
| `joinedDate`, `country` | Channel only |
| `avatarUrl`, `bannerUrl`, `isVerified`, `links` | Channel only |
| `playlistId`, `firstVideoId` | Playlist only |
| `scrapedAt` | Run timestamp |

There is no `commentCount`. Nothing this Actor reads publishes one, and a column that is permanently `null` is worse than an honest omission.

A `RUN_SUMMARY` record in the key-value store holds records saved by type, how many view counts were exact, how many videos were checked and found to have likes hidden, the filters that were applied, how many rows were removed by each filter and how many could not be date-checked at all, any input that ran into the paging ceiling before its cap was met, every failure with its reason, and any result card the parser did not recognise.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `searchQueries` | array | `[]` | Search terms, exactly as typed into YouTube's search bar |
| `startUrls` | array | `[]` | Video, channel, playlist or `@handle` URLs, mixed freely. Bare IDs and `youtu.be` links work too |
| `maxResultsPerInput` | integer | `50` | Cap on the total records one query or URL may produce, across every requested `resultTypes`. The main cost control, since charging is per record |
| `resultTypes` | array | `["video"]` | Any of `video`, `short`, `channel`, `playlist`. Decides what a search keeps, and which of a channel's tabs are read |
| `sortBy` | string | `relevance` | `relevance`, `date`, `views`, `rating`. Search terms only |
| `uploadedAfter` | string | | A date (`2026-01-15`) or a window (`30 days`) |
| `videoDuration` | string | `any` | `short` (under 4 min), `medium` (4 to 20), `long` (over 20). Search terms only |
| `videoFeatures` | array | `[]` | `hd`, `4k`, `subtitles`, `creativeCommons`, `live`, `360`, `hdr`, combined with AND. Search terms only |
| `includeVideoDetails` | boolean | `true` | **The accuracy dial.** On: exact view and like counts, full description, tags, category, exact publish date, one extra lookup per video. Off: faster and cheaper in time, with YouTube's rounded display counts and no publish date |
| `country` | string | `US` | Two-letter code. Changes which results rank |
| `language` | string | `en` | Two-letter code. Changes YouTube's interface strings, including the wording of relative dates |

At least one of `searchQueries` or `startUrls` is required. A run with neither fails with a named error rather than returning nothing.

```json
{
 "searchQueries": ["web scraping tutorial"],
 "startUrls": ["https://www.youtube.com/@Apify"],
 "maxResultsPerInput": 100,
 "resultTypes": ["video"],
 "uploadedAfter": "90 days",
 "sortBy": "views"
}
```

### Output

```json
{
 "type": "video",
 "id": "dQw4w9WgXcQ",
 "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
 "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
 "input": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
 "position": 1,
 "description": "The official video for “Never Gonna Give You Up” by Rick Astley...",
 "viewCount": 1802092331,
 "viewCountIsExact": true,
 "likeCount": 19322530,
 "durationSeconds": 213,
 "duration": "3:33",
 "publishedAt": "2009-10-25T06:57:33.000Z",
 "publishedText": null,
 "isLive": false,
 "wasLive": false,
 "tags": ["rick astley", "Never Gonna Give You Up", "rickroll"],
 "categoryName": "Music",
 "thumbnailUrl": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
 "channelId": "UCuAXFkgsw1L7xaCfnd5JJOw",
 "channelName": "Rick Astley",
 "channelHandle": "@RickAstleyYT",
 "channelUrl": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
 "subscriberCount": 4530000,
 "badges": [],
 "videoCount": null,
 "totalViewCount": null,
 "joinedDate": null,
 "country": null,
 "avatarUrl": null,
 "bannerUrl": null,
 "isVerified": false,
 "links": [],
 "playlistId": null,
 "firstVideoId": null,
 "scrapedAt": "2026-08-09T01:14:02.000Z"
}
```

A channel record fills the channel half of the same shape:

```json
{
 "type": "channel",
 "id": "UCTgwcoeGGKmZ3zzCXN2qo_A",
 "url": "https://www.youtube.com/channel/UCTgwcoeGGKmZ3zzCXN2qo_A",
 "title": "Apify",
 "channelName": "Apify",
 "channelHandle": "@Apify",
 "description": "Apify equips you and your AI agents with thousands of ready-to-run tools...",
 "subscriberCount": 17400,
 "videoCount": 268,
 "totalViewCount": 11646145,
 "joinedDate": "2017-01-04",
 "country": "Czechia",
 "avatarUrl": "https://yt3.googleusercontent.com/...",
 "bannerUrl": "https://yt3.googleusercontent.com/...",
 "isVerified": false,
 "links": [{ "title": "Browse tools", "url": "apify.com/store" }]
}
```

### Use cases

- **Creator discovery.** Search a topic, keep `channel` results, and rank by subscribers and lifetime views.
- **Topic tracking.** Run a set of queries on a schedule with `uploadedAfter`, and watch what gets published on a subject.
- **Channel analysis.** Point at a handle and pull the whole upload history with exact view and like counts, then look at the trend.
- **URL enrichment.** Hand it a list of video URLs from a spreadsheet and get structured metadata back, one row each.
- **Playlist inventory.** Turn a course playlist into a table of videos, durations and view counts.

### Limits and behaviour

- **Subscriber counts are rounded, by YouTube.** `17.4K subscribers` is the most precise figure published anywhere, so `subscriberCount` is `17400`, not the true number. It cannot be made exact.
- **`subscriberCount` on a search result needs `includeVideoDetails`.** A search listing does not carry it, so it is looked up once per channel and reused. With details off, search rows have `subscriberCount: null`; a channel's own pages always carry it either way.
- **`maxResultsPerInput` is a total, not a per-type quota.** One channel URL asked for `["channel", "video", "short", "playlist"]` with a cap of 200 returns at most 200 rows in all, filled in that order — not 200 of each. The cap is what you are charged for, so it is never exceeded.
- **View counts are exact only with `includeVideoDetails` on.** With it off, `viewCount` comes from a display label such as `474K views` and `viewCountIsExact` is `false`. Never round silently: check the flag.
- **`likeCount` needs `includeVideoDetails`.** A listing carries no like figure at all, in any form, so with details off the field is `null` on every row rather than a rounded guess.
- **`likeCount` is `null` when the creator hides likes.** That is not zero, and treating it as zero would misreport an unliked video.
- **`publishedAt` is `null` without video details.** A listing only carries `4 years ago`, and turning that into a timestamp would be precision this Actor does not have. `publishedText` carries YouTube's own wording instead.
- **`uploadedAfter` is exact with details on, approximate without.** With details off it is applied against the relative string, so a video near the boundary can fall either way. If that string cannot be read at all — which is what happens when `language` is not `en` — the row is kept rather than dropped, and counted in `RUN_SUMMARY.datesUnverifiable` so a result set that overshoots the window says why. An `uploadedAfter` value the Actor cannot parse stops the run before anything is fetched or charged.
- **`type` is `short` only when the source said so.** A `/shorts/` URL or a card from a Shorts shelf. A short clip found through search stays `video`.
- **`commentCount` is not returned.** See above.
- **A single input is walked for a bounded number of pages.** Very large caps on a broad search can stop short of `maxResultsPerInput`; when that happens the input is listed in `RUN_SUMMARY.truncatedByPageCap` rather than quietly returning a short answer. Split the work across narrower queries to go deeper.
- **No video, audio or subtitle downloads.** This Actor returns metadata. Nothing is streamed, saved or converted.
- **Deleted, private and region-locked videos are named failures**, listed in `RUN_SUMMARY.failures`, not blank rows. One bad URL never ends a run.
- **Results churn.** YouTube reorders its own search results between requests. Join on `id` and treat a single absence as noise.
- **Rows are deduplicated within an input, not across them.** Two search terms that both surface the same video produce two rows, each with its own `input` and `position`. Deduplicate on `id` yourself if you merge inputs.
- **Non-English runs return YouTube's own strings.** With `language` set to something other than `en`, `publishedText` comes back localised and any figure derived from a display label may be `null`. Turn video details on for language-independent numbers.
- **Public data only.** No authentication, and nothing that gets around access controls.

### FAQ

**Do I need a YouTube API key or a Google Cloud project?** No. You supply no credentials.

**How does this compare with the official Data API's quota?** There is no daily unit budget to plan against here. You are billed per record returned, so a 1,000-video run costs the same whether it is your first of the day or your fiftieth.

**Why is a view count a few hundred off from what the web page shows?** YouTube's own counters update on a delay and are not synchronised across surfaces. With `includeVideoDetails` on you get the precise figure YouTube itself reports for the video at that moment.

**How do I scrape a whole channel?** Put the channel URL or `@handle` in `startUrls` and raise `maxResultsPerInput` past the channel's upload count. Set `resultTypes` to `["video"]` for uploads, add `"short"` for its Shorts, `"playlist"` for its playlists, or `"channel"` for a single row about the channel itself. Remember the cap covers all of them together, so raise it if you ask for more than one.

**Can I get comments or transcripts?** No. Those are separate products with a different shape.

**Why is `likeCount` null on some videos?** The creator switched likes off. The field stays `null` rather than reporting a zero that is not true.

**Can I mix inputs?** Yes. Search terms, channel handles, playlist URLs and video URLs can all go into one run, and they produce the same record shape.

**Can I plug it into something else?** Yes. Apify API, the client libraries, webhooks, scheduled runs, dataset exports to JSON, CSV or Excel, or MCP. The output is structured JSON.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~youtube-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "searchQueries": ["web scraping tutorial"],
 "maxResultsPerInput": 50,
 "resultTypes": ["video"],
 "sortBy": "views",
 "uploadedAfter": "365 days"
 }'
```

### JavaScript example

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

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/youtube-scraper').call({
 startUrls: ['https://www.youtube.com/@Apify'],
 maxResultsPerInput: 200,
 resultTypes: ['video'],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const v of items) console.log(`${v.title} — ${v.viewCount} views, ${v.likeCount ?? 'likes hidden'}`);
```

# Actor input Schema

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

Search terms, exactly as you would type them into YouTube's search bar. Each term is searched independently, so two terms cost roughly twice one.

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

Video, channel, playlist or @handle URLs. The type is detected per URL, so you can mix them freely. Bare IDs and youtu.be links work too.

## `maxResultsPerInput` (type: `integer`):

Cap on the total records returned for each query or URL, counting every result type together. A channel with 4,000 uploads is paginated, so this is the main cost control since charging is per record.

## `resultTypes` (type: `array`):

Which kinds of result to keep. A search returns videos, channels and playlists together; a channel URL can yield its videos, its shorts, its playlists, or a record for the channel itself.

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

How YouTube should order search results. Applies to search terms only; a channel or playlist is always returned in its own order.

## `uploadedAfter` (type: `string`):

Keep only videos published after this point. Takes a date such as 2026-01-15 or a window such as '30 days'. Applied as YouTube's own coarse filter first, then exactly on each record's publish date.

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

Restrict search results by length: short is under 4 minutes, medium 4 to 20, long over 20. Search terms only.

## `videoFeatures` (type: `array`):

Restrict search results to videos carrying all of these features. Combined with AND, the way YouTube's own filter chips combine. Search terms only.

## `includeVideoDetails` (type: `boolean`):

Fetch each video's own record for exact view and like counts, the full description, tags, category and an exact publish date. Turn it off for a faster, coarser run: counts become YouTube's rounded display figures and the publish date is null.

## `country` (type: `string`):

Two-letter ISO 3166-1 country code. Changes which results rank, the way browsing YouTube from that country would.

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

Two-letter ISO 639-1 language code. Changes the interface strings YouTube returns, and with them the wording of relative dates such as '4 years ago'.

## Actor input object example

```json
{
  "searchQueries": [
    "web scraping tutorial",
    "python pandas"
  ],
  "startUrls": [
    "https://www.youtube.com/@Apify",
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "maxResultsPerInput": 50,
  "resultTypes": [
    "video"
  ],
  "sortBy": "relevance",
  "uploadedAfter": "90 days",
  "videoDuration": "any",
  "videoFeatures": [],
  "includeVideoDetails": true,
  "country": "DE",
  "language": "de"
}
```

# Actor output Schema

## `items` (type: `string`):

Every video, short, channel and playlist record the run produced.

## `runsummary` (type: `string`):

The RUN\_SUMMARY record from the run's key-value store.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchQueries": [
        "web scraping tutorial"
    ],
    "startUrls": [
        "https://www.youtube.com/@Apify"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/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 = {
    "searchQueries": ["web scraping tutorial"],
    "startUrls": ["https://www.youtube.com/@Apify"],
}

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/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 '{
  "searchQueries": [
    "web scraping tutorial"
  ],
  "startUrls": [
    "https://www.youtube.com/@Apify"
  ]
}' |
apify call arman-bd/youtube-scraper --silent --output-dataset

```

## MCP server setup

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