YouTube Channel Videos List avatar

YouTube Channel Videos List

Pricing

from $0.85 / 1,000 video record delivereds

Go to Apify Store
YouTube Channel Videos List

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

Tim Zinin

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

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.

How this Actor turns your input into a decision 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 /videos tab shows. A live measurement on @mkbhd found the uploads playlist carrying 100 videoIds against 30 on the /videos tab, and 9 of the playlist's first 30 videos (Shorts the /videos tab 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 @mkbhd on 2026-08-17 delivered ageText on 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 by videoId, 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 error string — 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.txt on www.youtube.com disallows the /api/, /youtubei/, /results and /comment paths those features require, and this Actor only ever requests /@handle/about, /@handle/videos and /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 /about page 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 /videos tab 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

  1. 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.
  2. Leave maxConcurrency at 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.
  3. Start the run from the Console, the Apify API, a scheduled Task, or an MCP client.
  4. 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.
  5. Check status and partial before treating a channel's rows as a complete picture. A partial:true video 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) — while status:"empty" or status:"error" on a channel's one summary row means that channel produced no video rows at all this run, and was not charged.
  6. 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.
  7. Check this run's key-value store record OUTPUT for 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/Name forms.
    • A bare channel ID — UC followed by 22 characters, e.g. "UCBJycsmduvYEL83R_U4JriQ" (@mkbhd's own, confirmed live). Supplying this form directly skips the /about resolution 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 leading UC and replacing it with UU, 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 to youtube.com/www.youtube.com/m.youtube.com is 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, default 5, 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"]
— the second value engineered to contain the literal substring "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

FieldMeaningImportant boundary
requestedChannelThe literal value you supplied in channelsAlways populated, on every row shape
channelIdThe resolved UC… channel IDnull only when resolution never completed (handle 404, SSRF block, invalid input)
canonicalChannelUrlYouTube's own canonical URL for the channelPassed through exactly as the source returns it, including its http:// scheme — never rewritten
uploadsPlaylistIdThis channel's uploads playlist id, "UU" + channelId.slice(2)Computed by character slice, never string replace — see the regression example above
videoIdThe video's own 11-character YouTube idnull on every channel-level summary row (error/empty)
position1-based rank in the uploads playlist as returned, 1–100null on summary rows; not guaranteed stable between runs if the channel uploads or deletes videos between them
titleThe video's titleNullable — 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"
viewCountTextYouTube's own rounded display text for view countNever an exact integer — "2.9M views", "7M views" are the source's own strings, not this Actor's rounding
ageTextYouTube's own rounded display text for publish recencyPopulated only for positions 1–30; null for every position beyond, by design, not by source failure
sourceWhere 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)
statusThe 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)
confidenceFixed marker on every video rowAlways "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
partialWhether this row's own data is incomplete for a stated reasontrue when ageText is absent, title is absent, or the channel returned fewer than the 100-video ceiling; never a silent gap — see action
actionSuggested 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)
errorHuman-readable reason, or nullPopulated only on status:"error" rows; null — never an empty string — on every other status, including "empty"
checkedAtISO-8601 timestamp of this row's own requestShared 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 shapeBillable?status / actionWhat it means
Video inside the age windowYes"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 titleYes"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 uploadsYes"partial"Every returned row is billed; the channel simply had fewer than the ceiling to give
Channel confirmed to have zero uploadsNo"empty" / "skip-not-found"Genuine HTTP 200, zero elements — never billed, error stays null
Channel handle/URL does not resolveNo"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 valueNo"error" / "skip-not-found"Blocked before any network request — see the SSRF-guard example above
Run-level spend cap reached mid-listNoone free explanatory summary rowNames 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

Where this Actor sits in your workflow

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 for adjacent workflows in social platforms and regional directories.

ActorWhat it does
YouTube Channel IntelPair 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 IntelligencePair 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 IntelligencePair 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.txt blocks 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.
  • title is occasionally absent for a specific video even at a clean HTTP 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.
  • position is not a stable identifier across runs; a channel uploading between two runs shifts every later position down by one. Track a video by videoId.
  • No dedicated field marks a row as a YouTube Short; Shorts are included (unlike the /videos tab, 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 plain 404) 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.