YouTube Channel Videos List
Pricing
from $0.85 / 1,000 video record delivereds
YouTube Channel Videos List
List up to 100 most recent uploads for any YouTube channel: videoId, title, view count, and publish age for the first 30. No API key, no login — just the channel handle, URL, or id.
Pricing
from $0.85 / 1,000 video record delivereds
Rating
0.0
(0)
Developer
Tim Zinin
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
YouTube Channel Videos List: Up to 100 Recent Uploads Per Channel, No API Key
Give this Actor a YouTube channel — handle, URL, or bare channel ID — and it returns up to 100 of that channel's most recent uploaded videos: video ID, title, view count, and, for the newest 30, publish age. No API key, no login, no quota to manage, no browser to drive.
The billable outcome is a delivered video row: one entry in the dataset with a real videoId from
the requested channel's own upload history, whether or not that row also carries publish age or a
title (both can legitimately be absent — see Real happy, partial and failure output). A channel that
cannot be resolved, or that genuinely has zero uploads, produces one free summary row instead and is
never charged.
What you get
- Up to 100 of a channel's most recent uploads, read from the channel's own uploads playlist —
not the 30 the
/videostab shows. A live measurement on@mkbhdfound the uploads playlist carrying 100videoIds against 30 on the/videostab, and 9 of the playlist's first 30 videos (Shorts the/videostab quietly filters out) were missing from that shorter list entirely. This Actor reads the playlist, not the tab, specifically so Shorts are not silently dropped from your count. - Publish age for the newest 30 positions, sourced primarily from the same page as the video list
itself — no second request needed in the common case. A live full run on
@mkbhdon 2026-08-17 deliveredageTexton all 30 of those positions from the uploads-playlist page alone; a separate/videos-tab request only fires as a fallback when the playlist page itself leaves a gap in that window, and is stitched back in byvideoId, never by row position (two different YouTube renders, no guaranteed matching order). - View counts as YouTube's own rounded display text —
"2.9M views","7M views"— because that is what the source publishes on these pages. This Actor never invents a precise number where YouTube itself shows a rounded one; the field is documented as display text, not exact data, everywhere it appears on this page. - A flat, honest row for every outcome, never a bare empty result. A channel that cannot be found,
a channel that genuinely has zero uploads, and a request blocked by the host-safety guard each
produce one clearly labelled summary row with a real
errorstring — never a silent zero-row dataset that looks the same as "this Actor is broken." - No purchase of what this Actor cannot honestly deliver. Transcripts and comments are not in the
output, and never will be from this specific request pattern —
robots.txtonwww.youtube.comdisallows the/api/,/youtubei/,/resultsand/commentpaths those features require, and this Actor only ever requests/@handle/about,/@handle/videosand/playlist, none of which are disallowed. That boundary is checked below in Evidence and boundaries, not assumed. - A channel supplied as a bare
UC…id skips one full request. This Actor's own resolution step only fetches the channel's/aboutpage to discover its channel ID when you give it a handle or a URL; a bare channel ID is already the identity it needs, so that step — and its ~2.5 MB page fetch — is skipped entirely.
Who uses it
- Content marketers and analysts tracking a competitor or a partner channel's upload cadence —
how often does this channel publish, and on what titles — without refreshing the channel's own
/videostab by hand or paying for a full "channel intel" product when all that is needed is the video list itself. - Media-monitoring and PR teams building a recurring digest of a set of channels' latest uploads, feeding a shared content-review queue or a weekly brief.
- Agencies running channel audits for clients, pulling a clean, structured snapshot of a channel's most recent output — title, view count, publish recency — as a starting point for a content strategy review.
- Developers and workflow builders who want a flat, machine-checkable video feed —
status,partial,action— that an n8n node, a Make scenario, or an agent can branch on directly, without handling YouTube's own Data API quota, OAuth, or the HTML shape of the page itself. - A confirmed, separate demand signal from "channel intel." This Actor's own market research found a comparable channel-video-listing product in this specific sub-niche serving several hundred monthly users on this platform — a distinct buyer intent from "find me this channel's contact email," which is what a channel-intel product answers instead. If you also need subscriber counts, business email, or external links from a channel's About page, that is a different Actor's job — see Evidence and boundaries.
How to run
- Provide
channels— one or more YouTube channels as@handle, a full channel URL (https://www.youtube.com/@handle), or a bare channel ID (UC…), up to 50 per run. The prefilled["@mkbhd"]needs no configuration and returns a real, non-empty 100-row dataset on the default input — this is not a placeholder that fails silently, it is a channel checked live before this page was written. - Leave
maxConcurrencyat its default of 5 unless you have a specific reason to raise it — this Actor's own stability has only been verified against sequential and moderately parallel requests, not a sustained high-concurrency load against YouTube. See Operating guide. - Start the run from the Console, the Apify API, a scheduled Task, or an MCP client.
- Read the Dataset for the result: one row per video (up to 100 per requested channel), or exactly one summary row per channel that could not be resolved or returned no uploads at all.
- Check
statusandpartialbefore treating a channel's rows as a complete picture. Apartial:truevideo row is still a real, billed video — it is only missing one specific thing (publish age beyond position 30, an occasionally-absent title, or the channel simply had fewer than 100 uploads to return) — whilestatus:"empty"orstatus:"error"on a channel's one summary row means that channel produced no video rows at all this run, and was not charged. - This Actor does not maintain a baseline between runs. Each run is a fresh snapshot of a channel's
current upload history; detecting "what's new since last time" is a downstream job — see Integration
recipes for a diffing pattern built on
videoId. - Check this run's key-value store record
OUTPUTfor a one-time run summary — requested/delivered/ paid/free/failed/incomplete counts, replay safety and any fatal error — written once at the end of the run, alongside the Dataset rows.
Pricing
$0.005 Actor start, plus $0.001 per delivered video row. The primary pricing noun is the
delivered video row: one dataset row carrying a real videoId from the requested channel's own
upload history, with status:"ok" or status:"partial" — a full row is billed the same as a
partial:true row, because both carry a genuine video identity, just not every optional field.
Every other row is free: a channel that cannot be resolved (status:"error"), a channel confirmed to
have zero uploads (status:"empty"), and any explanatory row this Actor writes when a run's own spend
cap is reached mid-way through a channel list. A run against the default ["@mkbhd"] input — the
channel this page's own examples were pulled from — delivers 100 video rows and is billed
$0.005 + (100 × $0.001) = $0.105 for that one channel; a run against ten channels that each return a
full 100 videos costs the same $0.001 per row, ten times over, plus one Actor start.
This price sits in the same corridor as this category's raw-content-feed comparables (a few tenths of a cent per result) rather than the higher per-contact pricing a "find me an email address" lead-gen Actor commands — because that is the honest shape of what this Actor delivers: a channel's own public upload list, not a contact.
On Apify's own billing record these are metered as two separate platform events — apify-actor-start
for the run fee, and result-found for each delivered video row — the same two lines you will see on
your invoice.
Input contract
{"channels": ["@mkbhd"],"maxConcurrency": 1}
This is the exact input this page's own live examples below were produced from — not an illustration.
{"channels": ["https://www.youtube.com/@ycombinator", "UCBJycsmduvYEL83R_U4JriQ"],"maxConcurrency": 10}
(This second example is a contract illustration built from .actor/input_schema.json — a full URL
form alongside a bare channel ID, at a higher concurrency setting — not itself the output of a
specific run; an input has no result of its own to reproduce.)
-
channels(array of strings, required, up to 50 items). Accepts three interchangeable forms:@handle— e.g."@mkbhd".- A full YouTube channel URL —
https://www.youtube.com/@handle,.../channel/UC…, or the legacy.../c/Name/.../user/Nameforms. - A bare channel ID —
UCfollowed by 22 characters, e.g."UCBJycsmduvYEL83R_U4JriQ"(@mkbhd's own, confirmed live). Supplying this form directly skips the/aboutresolution request entirely.
You never need to know or supply the channel's technical uploads playlist id (
UU…) — this Actor derives it itself from the resolved channel ID by slicing off the leadingUCand replacing it withUU, confirmed correct on this page's own live run (see Real happy, partial and failure output for the exact regression case). A value that looks like a URL but does not resolve toyoutube.com/www.youtube.com/m.youtube.comis rejected before any network request is made — see the SSRF-guard example below — and a bare value containing/,.., or://is rejected the same way, before it is ever substituted into a request URL. -
maxConcurrency(integer, default5, range 1–20). How many channels this run processes at once. The conservative default reflects what has actually been verified live — sequential and moderate parallelism — not a claim about a tested ceiling; see Operating guide before raising it for a large channel list.
Real happy, partial and failure output
Every row shares one flat, 16-field schema regardless of outcome, matching .actor/dataset_schema.json
exactly. Nothing below is invented — every JSON object on this page was pulled directly from this
Actor's own local runs against the exact input shown, immediately before this page was written. Video
titles, view counts and channel IDs are @mkbhd's own public data, unmodified.
Happy path — a video inside the age window (position 1 of 100)
{"requestedChannel": "@mkbhd","channelId": "UCBJycsmduvYEL83R_U4JriQ","canonicalChannelUrl": "http://www.youtube.com/@mkbhd","uploadsPlaylistId": "UUBJycsmduvYEL83R_U4JriQ","videoId": "o4SSoURPODY","position": 1,"title": "Google Pixel 11/Pro/Fold Impressions: It Is What It Is","viewCountText": "2.9M views","ageText": "5 days ago","source": "uploads-playlist","status": "ok","confidence": "display-text","partial": false,"action": "ingest","error": null,"checkedAt": "2026-08-17T20:21:35.818Z"}
canonicalChannelUrl is quoted exactly as YouTube's own page returned it — note the http:// scheme,
not https://. This Actor passes that field through unmodified rather than "fixing" it, because it is
the source's own value, not this Actor's construction.
Boundary row — the last position still inside the age window (position 30 of 100)
{"requestedChannel": "@mkbhd","channelId": "UCBJycsmduvYEL83R_U4JriQ","canonicalChannelUrl": "http://www.youtube.com/@mkbhd","uploadsPlaylistId": "UUBJycsmduvYEL83R_U4JriQ","videoId": "Hc0aqOEU2w8","position": 30,"title": "Bluey Phone Review","viewCountText": "4M views","ageText": "4 months ago","source": "uploads-playlist","status": "ok","confidence": "display-text","partial": false,"action": "ingest","error": null,"checkedAt": "2026-08-17T20:21:35.819Z"}
On this same live run, every one of positions 1 through 30 carried a non-empty ageText sourced
directly from the uploads-playlist page itself — no /videos-tab fallback request was needed for this
channel on this run, and source reads "uploads-playlist" on all 100 rows as a result. The
"uploads-playlist+videos-crossref" source value exists in this Actor's code for the case where the
playlist page itself leaves a gap in the age window (see Evidence and boundaries), but did not fire on
this specific run.
Partial path — a video outside the published age window (position 31 of 100)
{"requestedChannel": "@mkbhd","channelId": "UCBJycsmduvYEL83R_U4JriQ","canonicalChannelUrl": "http://www.youtube.com/@mkbhd","uploadsPlaylistId": "UUBJycsmduvYEL83R_U4JriQ","videoId": "eFUB_jL_XcM","position": 31,"title": "So This is Peak Foldable","viewCountText": "6.6M views","ageText": null,"source": "uploads-playlist","status": "partial","confidence": "display-text","partial": true,"action": "flag-no-age","error": null,"checkedAt": "2026-08-17T20:21:35.819Z"}
This row is billed the same as a full happy-path row — it carries a real videoId, title and
viewCountText. ageText:null here is a fixed, published contract boundary (position > 30), not a
source failure; partial:true and action:"flag-no-age" name the reason honestly rather than leaving
it to be inferred. The last row of this same run, position 100, carries the identical shape
("HP_mXUuHPbo", "Samsung broke this", "7M views", ageText:null, status:"partial") — the
partial shape holds all the way to the ceiling, not just near the boundary.
Silent-empty path — a syntactically valid but non-existent uploads playlist
{"requestedChannel": "UC0000000000000000000000","channelId": "UC0000000000000000000000","canonicalChannelUrl": "https://www.youtube.com/channel/UC0000000000000000000000","uploadsPlaylistId": "UU0000000000000000000000","videoId": null,"position": null,"title": null,"viewCountText": null,"ageText": null,"source": "uploads-playlist","status": "empty","confidence": null,"partial": false,"action": "skip-not-found","error": null,"checkedAt": "2026-08-17T20:18:10.546Z"}
This was live-checked directly against youtube.com/playlist?list=UU0000000000000000000000: a genuine
HTTP 200 response carrying zero video entries, not an error page and not a timeout. error is null
here — not a message — specifically because nothing went wrong; the source answered and the honest
answer was "no uploads under this identity." This is the same request pattern this Actor uses for every
real channel, run against a channel ID that does not correspond to any real uploader; it confirms the
uploads-playlist endpoint's silent-empty behavior on this Actor's own UU-derived id namespace
specifically, closing a verification gap this build's own spec had flagged as open before this page was
written. It does not, on its own, prove the same shape for a real, human-registered channel that
genuinely uploaded zero videos — that is a narrower claim this run does not make; see Limits.
Failure path — a channel handle that does not resolve
{"requestedChannel": "@this-handle-should-not-exist-zzz123","channelId": null,"canonicalChannelUrl": null,"uploadsPlaylistId": null,"videoId": null,"position": null,"title": null,"viewCountText": null,"ageText": null,"source": "channel-resolution","status": "error","confidence": null,"partial": false,"action": "skip-not-found","error": "channel not found (HTTP 404)","checkedAt": "2026-08-17T20:17:30.607Z"}
YouTube's own /about page for a handle that does not exist returns a genuine HTTP 404 — this Actor
reports that status verbatim in error rather than composing its own generic message, so a buyer
reading the row can tell exactly what the source itself said. This row was free — no charge, because no
video identity was ever established for it.
Guard path — a request blocked before it ever reaches the network
{"requestedChannel": "https://evil.example/@fake","channelId": null,"canonicalChannelUrl": null,"uploadsPlaylistId": null,"videoId": null,"position": null,"title": null,"viewCountText": null,"ageText": null,"source": "channel-resolution","status": "error","confidence": null,"partial": false,"action": "skip-not-found","error": "blocked host (not a youtube.com URL): evil.example","checkedAt": "2026-08-17T20:17:30.316Z"}
evil.example was never contacted. This Actor's input normalizer checks any URL-shaped channels
value against an explicit allow-list of YouTube hosts (youtube.com, www.youtube.com,
m.youtube.com) before it is substituted into any request template — a channel value that fails this
check produces this exact row and no outbound request at all, live-confirmed on this run.
Regression confirmation — the uploads-playlist id is a slice, never a find-and-replace
The same run that produced the silent-empty example above also carried
channels: ["UC0000...", "UCUCxxxxxxxxxxxxxxxxxxxx"]"UC"
a second time, inside its own tail. Its resulting row:
{"requestedChannel": "UCUCxxxxxxxxxxxxxxxxxxxx","channelId": "UCUCxxxxxxxxxxxxxxxxxxxx","canonicalChannelUrl": "https://www.youtube.com/channel/UCUCxxxxxxxxxxxxxxxxxxxx","uploadsPlaylistId": "UUUCxxxxxxxxxxxxxxxxxxxx","videoId": null,"position": null,"title": null,"viewCountText": null,"ageText": null,"source": "uploads-playlist","status": "empty","confidence": null,"partial": false,"action": "skip-not-found","error": null,"checkedAt": "2026-08-17T20:18:10.513Z"}
uploadsPlaylistId reads "UUUCxxxxxxxxxxxxxxxxxxxx" — only the leading two characters were replaced.
A naive global find-and-replace of "UC" with "UU" would have corrupted the embedded "UC" further
into the id as well, producing a wrong, unrecoverable playlist id on any real channel whose id happens
to contain that two-letter sequence again after position 2. This Actor slices the first two characters
off instead — confirmed correct on this live run, not just by inspection of the code.
Field dictionary
| Field | Meaning | Important boundary |
|---|---|---|
requestedChannel | The literal value you supplied in channels | Always populated, on every row shape |
channelId | The resolved UC… channel ID | null only when resolution never completed (handle 404, SSRF block, invalid input) |
canonicalChannelUrl | YouTube's own canonical URL for the channel | Passed through exactly as the source returns it, including its http:// scheme — never rewritten |
uploadsPlaylistId | This channel's uploads playlist id, "UU" + channelId.slice(2) | Computed by character slice, never string replace — see the regression example above |
videoId | The video's own 11-character YouTube id | null on every channel-level summary row (error/empty) |
position | 1-based rank in the uploads playlist as returned, 1–100 | null on summary rows; not guaranteed stable between runs if the channel uploads or deletes videos between them |
title | The video's title | Nullable — the uploads-playlist page occasionally omits it for a specific video even at HTTP 200; a missing title never demotes the row to status:"error" |
viewCountText | YouTube's own rounded display text for view count | Never an exact integer — "2.9M views", "7M views" are the source's own strings, not this Actor's rounding |
ageText | YouTube's own rounded display text for publish recency | Populated only for positions 1–30; null for every position beyond, by design, not by source failure |
source | Where this row's data came from | "uploads-playlist" (the common case), "uploads-playlist+videos-crossref" (fallback age enrichment fired), or "channel-resolution" (a summary row produced before the playlist was ever fetched) |
status | The outcome of this specific row | "ok" (full video row), "partial" (real video row missing one optional field or fewer than 100 returned), "empty" (channel resolved but has zero uploads), "error" (channel could not be resolved, or the request was blocked) |
confidence | Fixed marker on every video row | Always "display-text" — a reminder that viewCountText/ageText are the source's own rounded strings, never a computed exact value; null on summary rows, since there is no measurement to qualify |
partial | Whether this row's own data is incomplete for a stated reason | true when ageText is absent, title is absent, or the channel returned fewer than the 100-video ceiling; never a silent gap — see action |
action | Suggested downstream handling for this row | "ingest" (use as-is), "flag-missing-title", "flag-no-age", or "skip-not-found" (no video identity to act on) |
error | Human-readable reason, or null | Populated only on status:"error" rows; null — never an empty string — on every other status, including "empty" |
checkedAt | ISO-8601 timestamp of this row's own request | Shared granularity within one run, not a single run-wide timestamp — each channel is checked independently |
Evidence and boundaries
Why the uploads playlist, and not the /videos tab, is the primary source. A live measurement on
@mkbhd on 2026-08-17 pulled both pages: the uploads playlist (/playlist?list=UUBJycsmduvYEL83R_U4JriQ)
returned exactly 100 videoIds at HTTP 200, 2,066,685 bytes; the /videos tab
(/@mkbhd/videos?hl=en&gl=US) returned only 30, at 1,167,699 bytes. Comparing the two sets directly, 9
of the playlist's first 30 videos were absent from the /videos tab's own 30 — YouTube's own /videos
tab quietly filters out a channel's Shorts, while the uploads playlist does not. An earlier design for
this Actor line cross-referenced age data exclusively through the /videos tab and would have silently
dropped those 9 positions' age data as a result; this build instead reads age directly off the
uploads-playlist page's own lockupViewModel cards first, and only falls back to the /videos tab when
the playlist page itself leaves a gap — confirmed live to cover 30 of 30 positions in the age window
without that fallback ever firing, on the channel this page's own examples are built from.
Why the video ceiling is exactly 100, and why that is a source limit, not a policy choice. The
uploads-playlist page itself returns exactly 100 videoIds in one GET request; going further requires
pagination through /youtubei/v1/browse, and robots.txt on www.youtube.com disallows /youtubei/
outright. There is no design where this Actor could honestly offer more than 100 recent uploads per run
through this request pattern — 100 is the source's own single-request ceiling, not a number chosen for
this product.
Why publish age stops at position 30, even though the uploads-playlist page itself can carry it
further. This is a deliberate contract choice, not a second source limit. The /videos-tab fallback
this Actor uses when the playlist page leaves a gap only ever renders a channel's most recent ~30
uploads — so widening the published age window past 30 would make rows beyond it depend entirely on
whether the playlist page itself happened to carry age data that far, an inconsistency this Actor
chooses not to ship. Positions 31–100 report ageText:null honestly, every time, rather than
sometimes-yes-sometimes-no depending on an internal fallback the buyer cannot see.
Why view counts and publish age are rounded display text, never exact numbers. YouTube's own pages
do not publish an exact view count or an exact publish timestamp on these surfaces — they publish
"2.9M views" and "5 days ago", already rounded, already relative. This Actor reads exactly what the
source displays and reports it as confidence:"display-text" on every video row, rather than attempting
to reverse-engineer or estimate a precise figure the source itself does not expose here.
Why a title can be missing without the row becoming an error. An earlier live measurement across a
full 100-video pull on this same channel found 99 of 100 positions carrying a title, with one position
returning no title at all despite a clean row otherwise — the uploads-playlist page simply omits it for
that one entry. This Actor's own live run for this page (2026-08-17, quoted above) happened to return
all 100 titles — the condition is real but not deterministic, observed on one measurement and absent on
another against the same channel days apart. The code path exists specifically because it is real, even
on a run where it did not trigger: a missing title marks the row partial:true with
action:"flag-missing-title", never status:"error".
Why aboutChannelViewModel is matched by identity, never by page position. YouTube's /about page
carries the block this Actor uses to resolve a channel ID inside a JSON structure named
aboutChannelViewModel, which has been measured to occur on the page anywhere from one to two times —
the occurrence count is not a stable contract. An earlier design that always trusted "the second
occurrence" would silently return the wrong channel's identity — at a clean HTTP 200 — the moment the
page's own shape drifted to carrying only one. This Actor instead checks every occurrence's own identity
fields (channelId for a bare-id request, the @handle segment of canonicalChannelUrl for a handle
request) against what was actually requested, and only accepts an occurrence that provably names that
channel — refusing outright, with an honest error, rather than guessing by position, when nothing on
the page confirms the requested identity.
Why identity confirmation matters specifically for this Actor, more than for a channel-intel lookup.
The resolved channelId here is not just a display field — it is the literal input used to construct
uploadsPlaylistId, which then determines every one of up to 100 video rows this run produces. A wrong
channelId from a position-based guess would not just mislabel one field; it would silently hand a
buyer 100 videos from an entirely different channel, under their own requested channel's name.
The two-part negative case, both live-confirmed. A syntactically valid but non-existent uploads
playlist id (this page's own UC0000000000000000000000 and UCUCxxxxxxxxxxxxxxxxxxxx examples above)
returns HTTP 200 with zero video entries — a genuine silent-empty, reported as status:"empty", never
as an error. A handle that simply does not exist (@this-handle-should-not-exist-zzz123, also quoted
above) fails one step earlier, at channel resolution itself, with a genuine HTTP 404. This Actor's
code additionally recognizes a third shape neither example above happened to trigger: a syntactically
valid channel/UC… URL whose /about page returns HTTP 200 with the literal text "This channel does
not exist" in the body — status code alone is not sufficient to detect this case, so the body is checked
directly. This third path is documented in this Actor's own source (lib/channel.js,
bodyClaimsChannelDoesNotExist) but was not independently re-triggered live while writing this page.
The consent-cookie and locale requirement, and why its absence is dangerous rather than merely
inconvenient. Every request this Actor makes to YouTube carries ?hl=en&gl=US and a
Cookie: CONSENT=YES+1 header. Without both, the page renders in whatever language the runner's own IP
address implies, and this Actor's English-token parsing ("views", "ago") returns a clean, confident,
wrong empty result at HTTP 200 — a more dangerous failure mode than a network error, because
nothing about the response signals that anything went wrong.
Why this Actor does not, and structurally cannot, deliver transcripts or comments.
robots.txt on www.youtube.com disallows /api/, /youtubei/, /results, and /comment — the
paths transcript and comment retrieval require. This Actor's three request paths — /@handle/about,
/@handle/videos, and /playlist — are not on that disallowed list. This is not a feature this Actor
chose to leave out; it is a boundary the source's own published policy draws, and no version of this
Actor built on this request pattern can cross it.
vs. youtube-channel-intel. That Actor answers "who is this channel" — subscriber count, total
views, join date, business email, external links — from the same /about page's own
aboutChannelViewModel block this Actor also resolves a channel ID from. It never returns a video list.
This Actor answers "what has this channel posted recently" and never returns subscriber counts, contact
information, or external links. The channel-ID resolution step is structurally similar between the two
because both need it — but each is a separate purchase, and neither substitutes for the other.
vs. youtube-playlist-scraper. Both Actors ultimately request the same /playlist?list=… endpoint.
The difference is entirely in what goes into list=. This Actor accepts a channel and builds the
UU… uploads-playlist id itself — you never supply or see that identifier. youtube-playlist-scraper
accepts an arbitrary list= value you already have — someone else's public curated playlist, a
topic compilation, anything with a real YouTube playlist id — and performs no channel resolution at all.
If what you have is a channel, use this Actor. If what you have is a specific playlist URL that is not
necessarily anyone's own uploads, use that one instead.
vs. youtube-channel-lookalike-finder. That Actor reads the "similar channels" widget on the same
/about page — a completely different block (gridChannelRenderer) from the one this Actor reads. It
returns other channels' identities, never a video list for the requested channel itself.
Decision routing
| Row shape | Billable? | status / action | What it means |
|---|---|---|---|
| Video inside the age window | Yes | "ok" / "ingest" | Full row: title, view count, publish age all present |
| Video outside the age window (position 31–100) | Yes | "partial" / "flag-no-age" | Real video, published contract boundary, not a fault |
| Video with a missing title | Yes | "partial" / "flag-missing-title" | Rare, source-side omission at HTTP 200; code path exists, not observed on this page's own live run |
| Video from a channel with fewer than 100 uploads | Yes | "partial" | Every returned row is billed; the channel simply had fewer than the ceiling to give |
| Channel confirmed to have zero uploads | No | "empty" / "skip-not-found" | Genuine HTTP 200, zero elements — never billed, error stays null |
| Channel handle/URL does not resolve | No | "error" / "skip-not-found" | Source's own HTTP 404, or body text "This channel does not exist," quoted verbatim in error |
| Non-YouTube host supplied as a channel value | No | "error" / "skip-not-found" | Blocked before any network request — see the SSRF-guard example above |
| Run-level spend cap reached mid-list | No | one free explanatory summary row | Names how many channels were skipped and why; already-billed rows are unaffected |
A downstream workflow only needs two rules: act on any row where videoId is not null, and never
treat status:"partial" as a fault — it names a specific, honest reason a field is absent, not a
degraded run.
Commercial playbooks
Competitor upload-cadence tracking
A content or growth team runs this Actor on a short list of competitor or peer channels on a recurring
schedule, reading title and checkedAt across runs to answer "how often does this channel publish,
and on what" — without a human opening each channel's /videos tab by hand every week.
Content-strategy audits for agency clients
An agency onboarding a new client, or pitching a prospect, pulls a clean 100-video snapshot of the
channel's recent output as a starting artifact for a content review — title patterns, upload frequency
implied by ageText across the newest 30, and view-count spread across viewCountText — before writing
recommendations.
Media-monitoring digest across many channels
A PR or monitoring team feeds a list of up to 50 channels in one run, filters the dataset to
status:"ok" rows within the age window, and pushes the result into a shared review queue or a daily
digest — flat rows an existing pipeline can consume directly, with no HTML parsing of its own.
Feed input for a downstream "what's new" watch
Because this Actor is deliberately stateless between runs, a buyer who wants change detection — "alert
me only on genuinely new uploads" — builds that layer on top, diffing videoId sets between two runs'
datasets. This Actor's job stops at "here is this channel's current upload list, accurately and
honestly labelled"; deciding what changed since last time is a downstream concern this page does not
overstate as already solved.
Integration recipes

Scheduled channel digest
An Apify Task runs this Actor daily or weekly against a fixed channel list, reading the Dataset for
status:"ok"/"partial" rows and appending them to a content-calendar spreadsheet or a Slack/Notion
digest, keyed by videoId so a video already logged is never duplicated across runs.
Agent / MCP pattern
An agent supplies only channels (and optionally maxConcurrency), reads status and partial on
every row before treating it as complete, and never assumes ageText is present past position 30 —
action on each row names the exact reason when it is not, so the agent can branch on a fixed,
documented value instead of inferring intent from a null.
New-upload diffing
Because videoId is stable across runs for the same video, a workflow that keeps its own store of
previously seen ids per channel can compute a simple set difference after each scheduled run to surface
only genuinely new uploads — a pattern this Actor's own stateless design deliberately leaves to the
buyer's own downstream logic rather than half-implementing a baseline this Actor does not itself
maintain or bill for.
Data-warehouse append pattern
Dataset rows are appended keyed by videoId when present, requestedChannel + checkedAt otherwise (a
channel-level summary row carries no videoId); null is preserved as null rather than coerced to an
empty string, so a downstream query can distinguish "the source did not publish this field for this
video" from "this field is genuinely blank."
Operating guide
Choosing between a handle/URL and a bare channel ID. Supplying a bare UC… id skips the /about
resolution request entirely — one fewer ~2.5 MB fetch per channel, and one fewer place a request can
fail. If you already know a channel's id from a prior run's channelId field, reuse it directly on the
next run.
Why maxConcurrency defaults to 5, not higher. This Actor's live verification covered sequential
requests and moderate parallelism; it has not been load-tested against a sustained high-concurrency
burst directly against YouTube's own infrastructure. Raising it for a large channel list is honored, but
treat it as an unverified setting for very large batches — if you see a spike in status:"error" rows
with a network-level error message (timeout, connection reset) rather than a 404/blocked-host
message, lowering concurrency is the first thing to try before assuming a channel itself is broken.
Reading a partial:true row correctly. This flag has three independent possible causes —
ageText missing (action:"flag-no-age"), title missing (action:"flag-missing-title"), or the
channel simply returned fewer than 100 videos total. action on the row names exactly which applies; do
not assume partial:true means "something went wrong" — in the overwhelming majority of cases on any
real channel, it means "this video is beyond the 30-position age window," which is expected, not rare.
Why age enrichment via the /videos tab is conditional, not automatic. This Actor only issues the
extra /videos-tab request when the uploads-playlist page itself leaves a genuine gap somewhere in
positions 1–30. On the channel this page's own examples are built from, that gap did not occur — all 30
positions were covered by the playlist page alone, and source reads "uploads-playlist" on every row.
When the fallback does fire on a different channel, expect one additional ~1.2 MB request for that
channel only, and source:"uploads-playlist+videos-crossref" on the specific rows it filled in.
What changes between two runs of the same channel. position is not a stable video identifier
across runs — if the channel uploads a new video between two runs, everything shifts down by one
position. Track a specific video by videoId, never by position, in any downstream storage.
Related tools
Related tools for adjacent workflows in social platforms and regional directories.
| Actor | What it does |
|---|---|
| YouTube Channel Intel | Pair it in the social platforms and regional directories workflow: Public intel card for any YouTube channel: subscribers, total views, video count, join date, country, full... |
| Telegram Export Intelligence | Pair it in the social platforms and regional directories workflow: Turn owner-authorized Telegram message exports into per-post reach context, evidence, confidence, review... |
| Authorized Professional Directory Intelligence | Pair it in the social platforms and regional directories workflow: Turn buyer-authorized or licensed professional directory exports into privacy-aware evidence, freshness,... |
FAQ
Does this Actor return video transcripts or comments?
No, and it structurally cannot from this request pattern. robots.txt on www.youtube.com disallows
the /api/, /youtubei/, /results, and /comment paths those features require; this Actor only ever
requests /about, /videos, and /playlist, none of which are disallowed. See Evidence and boundaries
for the exact robots.txt lines.
Why is the ceiling exactly 100 videos, and not higher?
Because that is the source's own single-request limit on the uploads-playlist page. Retrieving more
requires paginating through /youtubei/v1/browse, a path robots.txt disallows. There is no
configuration of this Actor that returns more than 100 recent uploads per channel per run.
Why does publish age stop at position 30 when the playlist page can sometimes carry it further?
Because the fallback path this Actor uses when the playlist page leaves a gap — the /videos tab — only
ever renders a channel's most recent ~30 uploads. Extending the published window past 30 would make
some runs' coverage depend on an internal fallback the buyer cannot observe. This Actor ships a fixed,
predictable boundary instead: ageText for 1–30, honestly null beyond, every time.
Are the view counts exact numbers?
No. viewCountText is YouTube's own rounded display string — "2.9M views", "7M views" — exactly as
the source publishes it. confidence:"display-text" on every video row is a permanent reminder of this,
not a value that ever changes to something more precise.
What happens for a channel that has never uploaded anything?
A syntactically valid but non-existent uploads-playlist id returns a genuine HTTP 200 with zero
elements, live-confirmed twice while writing this page (see the silent-empty example above) — this
Actor reports status:"empty", error:null, and charges nothing. This confirms the request pattern's
behavior on a non-existent identity; a real channel that genuinely uploaded zero videos was not
independently located and tested live for this page, though the request this Actor sends is identical
either way — see Limits.
Does this include YouTube Shorts?
Yes, because this Actor reads the uploads playlist, not the /videos tab — the tab is the surface that
filters Shorts out, live-confirmed missing 9 of a channel's first 30 uploads on that shorter list. There
is no separate field marking a row as a Short specifically; a Short appears as an ordinary video row
with its own videoId, title, and counts.
Can I tell which videos are new since my last run?
Not from a single run's fields directly — position shifts as a channel uploads, and this Actor keeps
no baseline between runs by design (see Pricing: a stateless per-video price, not a subscription-watch
price). Diff videoId sets between two runs' datasets in your own storage to build that layer; see
Integration recipes.
What happens with an invalid or non-YouTube URL in channels?
It is rejected before any network request, free of charge, with error naming the exact reason —
"blocked host (not a youtube.com URL): …" for a wrong domain, or a path-separator/scheme message for a
bare value that looks like it is trying to smuggle a path. See the guard-path example above for the
exact live row.
Is this the same data as YouTube's official Data API?
No — this Actor reads the same public pages a browser renders, not YouTube's own quota-metered Data API. That means no API key and no quota to manage, and also means the shape of the output depends on YouTube's own page structure remaining stable, the same dependency any page-reading Actor carries — see Limits.
Sources and rights
Source family: YouTube public channel and playlist pages. This Actor reads the channel's own uploads playlist page, anonymously, with no login, no API key and no browser session.
The only source. www.youtube.com — specifically /{@handle|channel/UC…}/about (channel
resolution only, skipped for a bare channel-ID input), /playlist?list=UU… (this channel's own uploads
playlist, the primary video-list and age source), and conditionally /{@handle|channel/UC…}/videos
(fallback age enrichment only). No YouTube Data API key, no OAuth, no login.
robots.txt, checked directly. www.youtube.com/robots.txt disallows /api/, /youtubei/,
/results, and /comment. It does not disallow /@handle/about, /@handle/videos, /watch, or
/playlist — the four path shapes this Actor's three request types are built from. This was read
directly from the published policy, not assumed from general knowledge of the platform.
What this data is. Everything this Actor returns — video titles, rounded view counts, rounded publish age, a channel's own canonical URL — is public metadata YouTube itself displays to any visitor of these pages, with no login required to view it. This Actor does not access, and this page makes no claim about, any content beyond what these three specific public page types already show a signed-out visitor.
No redistribution license was found or is claimed here. Unlike a government open-data source with an explicit published reuse licence, this Actor's authors did not find, and this page does not claim, any specific licence grant from YouTube covering redistribution of this page data. What is delivered is exactly the public metadata these specific pages display — buyers are responsible for their own review of how they use it downstream.
No warranty of continued page stability. This Actor's extraction depends on the current JSON
structures YouTube's own front end embeds in these pages (aboutChannelViewModel, lockupViewModel,
contentMetadataViewModel, and related renderer keys). YouTube can change this structure without
notice; this Actor's own parsing tries multiple known key shapes in a documented order specifically to
absorb minor rollout variation, but a substantial front-end redesign of these specific pages could
degrade or break extraction until this Actor is updated.
Limits
- No transcripts, no comments, no engagement metrics beyond a rounded view count —
robots.txtblocks the request paths those would require, on this host, for any Actor built this way. - Hard ceiling of 100 most recent uploads per channel per run — the uploads-playlist page's own
single-request limit; further pagination requires a
robots.txt-disallowed path. - Publish age (
ageText) is populated only for positions 1–30, by a fixed contract boundary, not a moving source limit — see Evidence and boundaries for why this Actor does not widen it opportunistically. - View counts and publish age are YouTube's own rounded display text, never exact numbers or exact timestamps.
titleis occasionally absent for a specific video even at a cleanHTTP 200— measured once at 99 of 100 positions on one run, and at 100 of 100 on this page's own live run days later; the condition is real but not deterministic, and never fails the row.- This Actor keeps no baseline between runs — each run is an independent snapshot; "what's new since
last time" is a downstream computation on
videoId, not a built-in feature. See Integration recipes. positionis not a stable identifier across runs; a channel uploading between two runs shifts every later position down by one. Track a video byvideoId.- No dedicated field marks a row as a YouTube Short; Shorts are included (unlike the
/videostab, which filters them) but are not distinguished from long-form uploads in the row shape. - The genuine-zero-upload silent-empty behavior was live-confirmed on a syntactically valid but non-existent uploads-playlist id, twice, while writing this page — not independently re-confirmed against a real, human-registered channel known to have zero uploads, though the request pattern used is identical in both cases.
- The
HTTP 200+ "This channel does not exist" body-text negative case (a third resolution-failure shape, distinct from a plain404) is implemented in this Actor's code but was not independently re-triggered live while writing this page. - Extraction depends on YouTube's current front-end JSON shapes remaining stable; a major redesign of these specific public pages could require an update to keep working.
Support boundary
Actor support covers deterministic input validation, the source-access and extraction behavior
described on this page, the Dataset schema, and the billing behavior of the delivered-video-row pricing
model for an accepted release. It cannot guarantee YouTube's own page structure will remain unchanged,
cannot restore extraction if YouTube redesigns these specific public pages before this Actor is updated
to match, cannot provide content-strategy or competitive advice beyond the raw data delivered, and
cannot retrieve transcripts, comments, or any data behind a robots.txt-disallowed path regardless of
how the request is asked. When reporting a problem, include the Actor run ID, the exact channels value
that produced it, the status/action/error on the affected row, and the checkedAt time.