# YouTube Channel Scraper – Videos, Views & Likes (`simple.actor/youtube-channel-videos`) Actor

Scrape the latest videos from any public YouTube channel — title, link, exact view count, likes, publish date, duration, description and thumbnail — one row per video. No login, no cookies, no API key and no browser: a channel listing costs one request. Unofficial.

- **URL**: https://apify.com/simple.actor/youtube-channel-videos.md
- **Developed by:** [Simple Actor](https://apify.com/simple.actor) (community)
- **Categories:** Social media, Videos, News
- **Features:** Limited permissions
- **Stats:** 12 total users, 7 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 videos

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## YouTube Channel Scraper – Videos, Views & Likes

Scrape the latest videos from any public YouTube channel — **title, link, exact
view count, likes, publish date, duration, description, category and
thumbnail** — one row per video. Built for channel monitoring, competitor and
content research, influencer vetting and video dataset building.

No login, no cookies, no API key and **no browser**. Everything on a channel
listing is in a JSON object inside the page YouTube serves to a logged-out
reader, so a whole channel costs one request.

*Unofficial. Not affiliated with, endorsed by, or sponsored by YouTube or
Google.*

### Features

- **Latest videos per channel** — up to 30, newest first, read from the page
  YouTube serves a logged-out reader.
- **Exact view counts and publish dates** — not YouTube's rounded "53K views"
  and "3 days ago", but 53,674 and the real date.
- **Like counts and descriptions** — which appear nowhere on the channel
  listing — with every link in the description written out in full.
- **Every address form works** — `@handle`, `/channel/UC…`, and the legacy `/c/`
  and `/user/` forms, so a list saved a decade ago still resolves.
- **Bare handles and channel IDs accepted** — paste a spreadsheet column
  straight in.
- **Channel context on every row** — name, id, handle, subscriber count and the
  channel's total video count, so you can tell 30-of-30 from 30-of-6,100.
- **Rounded figures are always labelled** — `viewCountIsApproximate` and
  `publishedAtIsApproximate` mean you are never silently misled.
- **A cheap freshness mode** — turn details off and a whole channel is one
  request.
- **Incremental polling** — `onlyVideosNewerThan` takes `7 days`, `3 months` or
  an ISO date, and refuses rather than returning a misleading short list.
- **No browser, 256 MB** — fast and light.
- **Runs stay green.** A nonexistent channel or a pasted video URL is a row.

### Input

```json
{ "handles": ["NASA"], "maxVideos": 30 }
```

All four of YouTube's address forms work, so a list saved at any point in the
last decade still resolves:

```json
{
  "startUrls": [
    { "url": "https://www.youtube.com/@NASA" },
    { "url": "https://www.youtube.com/channel/UCLA_DiR1FfKNvjuUpBHmylQ" },
    { "url": "https://www.youtube.com/user/NASAtelevision" }
  ]
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `startUrls` / `handles` | array | — | The channels. `handles` also takes bare handles (`NASA`) and channel IDs (`UCLA_…`). |
| `maxVideos` | integer | `30` | Newest first. 30 is the ceiling — see [What it cannot do](#what-it-cannot-do). |
| `includeVideoDetails` | boolean | `true` | Exact views, likes, description and publish date. One request per video, and what the price turns on — see [What it costs](#what-it-costs). |
| `onlyVideosNewerThan` | string | — | `7 days`, `3 months`, or an ISO date. |
| `proxy` | object | Apify datacenter | What it is built and measured on. Residential is available but rarely worth it — see the FAQ. |

### Output

```json
{
  "type": "video",
  "videoId": "FkgVB19I6xw",
  "url": "https://www.youtube.com/watch?v=FkgVB19I6xw",
  "title": "NASA's Roman Space Telescope: A New Perspective on the Cosmos",
  "viewCount": 53674,
  "viewCountIsApproximate": false,
  "likeCount": 1839,
  "publishedAt": "2026-08-24T21:00:22.000Z",
  "publishedText": "3 days ago",
  "publishedAtIsApproximate": false,
  "durationSeconds": 46,
  "category": "Science & Technology",
  "thumbnailUrl": "https://i.ytimg.com/vi/FkgVB19I6xw/hq720.jpg",
  "channelName": "NASA",
  "channelId": "UCLA_DiR1FfKNvjuUpBHmylQ",
  "channelHandle": "@NASA",
  "subscriberCount": 15100000,
  "channelVideoCount": 6100,
  "scrapedAt": "2026-08-28T15:40:29.000Z"
}
```

**Store `channelId`, not the handle.** A channel's owner can change its handle;
the `UC…` id never changes.

#### Exact figures, and when you get them

YouTube rounds everything it displays. A channel listing says "53K views" and
"3 days ago" — 53,674 and 53,001 both print as 53K. The exact numbers live only
on each video's own page, which is what `includeVideoDetails` fetches, along
with the like count and description that exist nowhere else.

A video's page carries those figures twice: once in the data the page itself is
drawn from, and once in YouTube's player data. The Actor reads whichever
arrives. The page's own data comes with nearly every read; the player data is
withheld from most datacenter requests, and it is the only place the publish
*time*, the category and the keywords appear.

So every row says which it got:

| | `viewCountIsApproximate` | `likeCount` | `publishedAt` | `publishedAtIsApproximate` | `category` |
|---|---|---|---|---|---|
| read with player data | `false` | a number | the exact instant | `false` | set |
| read without it | `false` | a number | the publish day, at 00:00 UTC | `true` | `null` |
| page could not be read | `true` | `null` | from "3 days ago" | `true` | `null` |

The publish day is the calendar day YouTube displays, which is the day in
Pacific time. A video less than a day old keeps the listing's "2 hours ago"
instead, which places it more precisely than a bare day would.

Only the last row is not charged the detail fee. You are never misled: a
rounded figure is always flagged as one.

A video with **no public view count at all** — a members-only upload, say —
comes back with `viewCount: null` and `viewCountIsApproximate: null`. A null
flag means there is no figure to describe, not a rounded one waiting to be
improved.

### How to use

#### From Apify Console

1. Open the Actor and click **Try for free** / **Start**.
2. Paste channel addresses into **Channels**, or a column of bare handles and
   channel IDs into **Channels (plain list)** — both are read together. Give a
   channel, not a video.
3. Leave **Exact views, likes and dates** on for real figures, likes and
   descriptions. Turn it **off** for a cheap freshness sweep
   across many channels — a channel then costs one request.
4. To poll for what is new, set **Only videos newer than** to a window shorter
   than the gap between your runs.
5. Click **Start**, then open the **Dataset** tab and export as JSON, CSV or
   Excel.

**Store `channelId`, not the handle** — the handle can change, the `UC…` id
cannot.

#### From the API

```bash
curl -s "https://api.apify.com/v2/acts/simple.actor~youtube-channel-videos/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"handles": ["NASA"], "maxVideos": 30}'
```

A cheap daily freshness check across many channels, with the JavaScript client:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('simple.actor/youtube-channel-videos').call({
    handles: ['NASA', 'veritasium', 'UCLA_DiR1FfKNvjuUpBHmylQ'],
    includeVideoDetails: false,   // one request per channel, rounded figures
    onlyVideosNewerThan: '2 days',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();

for (const row of items) {
    if (row.error) { console.warn(row.error, row.errorDescription); continue; }
    console.log(row.channelName, row.title, row.publishedText);
}
```

### Use cases

- **Monitor channels for new uploads** — poll a list on a schedule with
  `onlyVideosNewerThan` and `includeVideoDetails: false` for a cheap check, then
  read the details of anything new.
- **Competitor and content research** — titles, durations, descriptions and
  exact view counts across rival channels.
- **Influencer vetting** — subscriber count, total video count and real
  engagement on recent uploads, rather than YouTube's rounded display figures.
- **Video performance benchmarking** — exact views and likes per video, loaded
  straight into a spreadsheet or warehouse.
- **Building a video dataset** — metadata for a channel's latest 30 uploads with
  stable `videoId` and `channelId` keys.
- **Media and brand monitoring** — watch newsroom, agency and brand channels for
  what they published today.

### Usage notes

#### What it costs

Prices from 24 October 2026, lower on Apify's higher subscription plans:

| Your Apify plan | each video returned | exact figures, on top | per 1,000 videos, with exact figures | per 1,000 videos, without |
|---|---|---|---|---|
| Free | $0.0004 | $0.0008 | **$1.20** | **$0.40** |
| Starter (Bronze) | $0.0004 | $0.0008 | **$1.20** | **$0.40** |
| Scale (Silver) | $0.00035 | $0.0006 | **$0.95** | **$0.35** |
| Business (Gold) | $0.0003 | $0.00045 | **$0.75** | **$0.30** |
| Enterprise (Platinum, Diamond) | $0.0003 | $0.00045 | **$0.75** | **$0.30** |

With exact figures you get exact views, likes, descriptions and publish dates.
Without them you get titles, links, durations, thumbnails and rounded figures.

That split is the real cost, passed on: a channel listing is one request for up
to thirty videos, while the exact figures are a request each. If you are
watching a lot of channels to see what is new, turn `includeVideoDetails` off
and a channel costs one request.

Nothing else is charged. A channel that could not be read, a video with no
detail page available, a run whose every input was unusable — none of them bill.

#### What it cannot do

**Thirty videos per channel.** That is what a channel page renders to a
logged-out reader; going further needs YouTube's internal browse API with a
signed continuation token, which is a far more fragile surface than reading a
page. Every row carries `channelVideoCount`, so you can always tell whether you
are seeing all of a channel's videos or the latest thirty of many.

If you ask for a window reaching further back than those thirty
(`onlyVideosNewerThan: "10 years"`), the channel comes back as an error row
(`window_too_wide`) rather than a short list that would look like a complete
answer for that window.

#### Failures

**The run always succeeds.** A channel that does not exist, a string that could
never be a handle, a video URL pasted where a channel belongs — each is a row,
and the run's status message counts them. Only a bug in this Actor fails a run.

**A run finishes inside its timeout.** It stops reading shortly before the
timeout you set and delivers what it holds. Videos it had no time to read
exactly keep the listing's figures, flagged and not charged; channels it never
reached each get a `timed_out` row.

> **Act on the presence of an `error` field, never on the run status.** A
> channel read fine that has posted nothing produces no rows; a channel that
> could not be read produces exactly one row, carrying `error` and no `videoId`.

| `error` | what happened |
|---|---|
| `not_found` | no such channel |
| `bad_input` | the text given could never be a channel, or is a video/playlist URL |
| `window_too_wide` | the window reaches further back than one page of videos shows |
| `read_failed` | YouTube could not be reached, or served a page in a language this Actor could not read |
| `timed_out` | the run reached its timeout before this channel was read — raise the timeout, or give fewer channels per run |
| `parse_failed` | the Actor hit an error it did not expect on this channel; the other channels in the run are unaffected |

### FAQ

**Is scraping YouTube legal?**
This Actor reads only the data YouTube already serves to a logged-out visitor in
the page itself — it does not log in, use cookies, or reach private or
members-only content. YouTube's Terms of Service restrict automated collection,
so check the platform's ToS and your own obligations before using it.

**Do I need a YouTube Data API key or a Google Cloud project?**
No. There is no key, no OAuth and no quota of your own to manage.

**Can I get more than 30 videos per channel?**
No. Thirty is what a channel page renders to a logged-out reader; going further
needs YouTube's internal browse API with a signed continuation token, which is a
far more fragile surface than reading a page. Every row carries
`channelVideoCount`, so you can always tell whether you are seeing all of a
channel's videos or the latest thirty of many.

**Does it support pagination?**
Not beyond that first page, for the reason above. To follow a channel over time,
run it on a schedule with `onlyVideosNewerThan`.

**Why is `viewCount` rounded on some rows?**
Because the exact figure lives only on each video's own page, and that page
could not be read — YouTube or the proxy did not answer in the time allowed.
Those rows keep the listing's rounded figures and say so via
`viewCountIsApproximate: true`, with `likeCount: null`, and are not charged the
detail fee. You are never silently given a wrong number.

**Why is `publishedAt` a date at midnight on some rows, and `category` null?**
The publish time of day, the category and the keywords come only with YouTube's
player data, which it withholds from most datacenter requests. Those rows carry
the publish *day* — exact to the day, flagged `publishedAtIsApproximate: true`
because it is not the instant — with the exact views, likes and description
beside it.

**What does `viewCountIsApproximate: null` mean?**
That there is no figure to describe at all — a members-only upload, for example,
publishes no view count. A null flag is not a rounded number waiting to be
improved.

**Should I turn on residential proxy?**
Only if you need the publish time of day, the category or the keywords on more
rows. It does not improve the view or like counts, which are exact on
datacenter, and it costs roughly ten times as much per video.

**Should I store the handle or the channel ID?**
The `channelId`. A channel's owner can change its handle at any time; the `UC…`
id never changes.

**Why did my run succeed when a channel was not read?**
By design. A channel that does not exist, a string that could never be a handle,
or a video URL pasted where a channel belongs are each answers about that input,
not faults — so each is a row and the run stays green. Act on the presence of an
`error` field, never on the run status.

# Actor input Schema

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

Channel addresses, one per entry. All four of YouTube's forms work: https://www.youtube.com/@NASA, /channel/UC…, and the legacy /c/ and /user/ forms. Give a channel, not a video — a watch URL comes back as an error row telling you so.

## `handles` (type: `array`):

The same thing as a plain list — easier to paste a column out of a spreadsheet. Accepts "@NASA", bare "NASA", or a channel ID like UCLA_DiR1FfKNvjuUpBHmylQ. Both inputs are read; giving either is enough.

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

How many of the channel's latest videos to return, newest first. A channel page renders 30 to a logged-out reader and offers no way to page further without YouTube's internal API, so 30 is the ceiling — every row carries channelVideoCount so you can tell when you are seeing all of them.

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

On by default. A channel listing rounds every view count ("53K views") and gives only a relative date ("3 days ago"). This reads each video's own page for the exact figures — 53,674 views and the real publish date — and adds the like count and description, which appear nowhere else.

It is also what the price turns on. Reading a channel listing is one request for up to thirty videos; the exact figures are a request each, and measured that is five times the cost. So a video costs $0.0005 either way and the exact figures add $0.0010 on top — $1.50 per 1,000 videos with them, $0.50 per 1,000 without. Turn this off for a cheap freshness check across many channels.

Every read that lands carries the exact view count, the like count, the description and the publish day. The publish time of day, the category and the keywords come only when YouTube also sends its player data, which it withholds from most datacenter requests; those rows carry the date as a day and say so via publishedAtIsApproximate. A video whose page could not be read at all keeps the listing's rounded figures, says so via viewCountIsApproximate, and is not charged the detail fee.

## `onlyVideosNewerThan` (type: `string`):

Keep only videos published after this point — an ISO date like 2026-08-01, or a window like "7 days", "3 months". If the window reaches further back than the 30 videos a channel page shows, the channel comes back as an error row (window_too_wide) rather than a short list that would look like a complete answer.

## `proxy` (type: `object`):

Defaults to Apify's datacenter proxy, which is included in every plan and is what this Actor is built and measured on. Channel listings, and each video's exact views and likes, read reliably on it.

Residential costs roughly ten times as much per video and does not improve those figures. What it adds is YouTube's player data on more videos — the publish time of day, the category and the keywords.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.youtube.com/@NASA"
    }
  ],
  "handles": [
    "@NASA"
  ],
  "maxVideos": 30,
  "includeVideoDetails": true,
  "onlyVideosNewerThan": "7 days",
  "proxy": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per video: channel, title, date, views and link.

## `raw` (type: `string`):

Every field of every row, exactly as pushed.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.youtube.com/@NASA"
        }
    ],
    "handles": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("simple.actor/youtube-channel-videos").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 = {
    "startUrls": [{ "url": "https://www.youtube.com/@NASA" }],
    "handles": [],
}

# Run the Actor and wait for it to finish
run = client.actor("simple.actor/youtube-channel-videos").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 '{
  "startUrls": [
    {
      "url": "https://www.youtube.com/@NASA"
    }
  ],
  "handles": []
}' |
apify call simple.actor/youtube-channel-videos --silent --output-dataset

```

## MCP server setup

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

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/zaNWrmjAeHPzx01RZ/builds/Fus5z95pSI1FRZHKM/openapi.json
