Every entry here is one build on the Builds tab, 1:1, newest first.
- A run that cannot open the memory of what you already have now says so, instead of quietly
charging for it again. The promise on this page is that a comment your account already has is
never bought twice, and the memory that keeps it is a key-value store in your own Apify account.
A run started with a scoped API token in restricted-access mode cannot open that store at all —
so every comment you already owned was collected and charged a second time, and nothing on the
run page, in the rows or on the receipt told you why. It now does: an uncharged note row at the
top of the dataset, a clause on the run page and a line on the receipt all name the cause and the
fix — give the token key-value store Read, Write and Create permission, or set Actor runs to Full
access, under Settings → API & Integrations, or start the run from the console. Every other reason
the memory can be unavailable keeps the words it had. Nothing else moved: no input field, output
column, charged event or price changed.
- And a token that can READ that memory but not WRITE it now says so too. That one is the one
that breaks "never pay for the same comment twice" while looking like a perfect run: every comment
you already had was still recognised and skipped, but nothing this run delivered could be written
down — so your NEXT run bought and charged all of it again, with nothing anywhere to say why. The
run now names it the moment the first write is refused, on the run page, on its own uncharged row,
on the receipt and in the run log, and tells you the permission to add: key-value store Write (and
Create) under Settings → API & Integrations, or Actor runs set to Full access. A run with a full
token is unaffected, and nothing about what is charged changed.
- The pointer to the channel-transcript actor now names it by its current title. If what you
want is every video on a channel as text rather than its comments, this page points you at that
actor — and it is now called YouTube Channel Videos Transcript Scraper — Bulk Video to Text,
renamed so the name says what a plain run there delivers (the newest 50 videos per channel, up
to 5,000). Same actor, same link; only the words changed. Nothing here moved: no input field,
output column, charged event or price changed.
- The pointer to the transcript actor now names it by its current title. If what you want is the
spoken words inside a video rather than its comments, this page points you at that actor — and it is
now called YouTube Transcript API & Scraper — YouTube Subtitles, Speech AI. The link goes to the same
actor as before; only the words a reader sees changed.
- Nothing else moved: no input field, output column, charged event or price changed.
- A link in your rows can never carry a network address, whatever form it arrives in. Every link
this actor delivers — the video link, the author link, and the link you pasted handed back to you —
already went through our own URL hygiene before reaching your dataset, and that pass had a gap:
where a link is written with HTML-escaped separators (
& in place of &), the address
parameter was not recognised by name and could survive into a row. It is now removed however the
link is written — escaped, plain or legacy separators, upper or lower case, and inside a link that
wraps another link. Nothing else in a link moves: the expiry, the signature and every routing
parameter are delivered exactly as they were served, so the links keep working exactly as before.
No column, no input, no price and no charge changes.
- Maintenance only: nothing about your runs, your rows, your events or your prices changes. This
actor shares its connection layer with our YouTube transcript actors, and that layer was fixed
there: a minimum gap is now held before every kind of request, and a connection that answered can
be carried over to the next video instead of a new one being opened each time. Neither half moves
anything here — this actor makes only the one kind of request, which already waited that gap, and
its own retry ladder already carried a connection that answered from one video to the next. The
shared file is updated here so the two stay identical, which is how a fix on one of them reaches
the other.
- A run that stopped because your account already had everything now says so in the run's own
record, not just in the row. Last build the uncharged row learned to say that the ten-pages-of-
repeats bound is this actor's own and not YouTube's — but the run's summary still filed it under
the platform running out of comments, so the row and the summary said opposite things about the
same video. The summary now names it separately, as a stop rather than a miss: nothing failed,
nothing ran out, and nothing was charged. "Depth reached" from now on means only what it says —
YouTube's own comment order running out, or this actor's own page limit on one video.
Your inputs, your events, your prices and every column on your rows are unchanged, and nothing you
have already collected needs re-running.
- A re-run whose first pages hold only comments you already have now says so, instead of blaming
YouTube's ceiling. This actor stops walking a video after ten pages running that bring back
nothing new, so it does not keep buying pages you would not be charged for anyway — but the
uncharged row it left said YouTube had stopped serving that video, which was not true and told you
a re-run in a different order would not help. The row now names the real reason, how many comments
were skipped because your account already had them, and the three things that do help: Include
comments you already have, the other comment order, or a later run once new comments have
arrived. The run page's own line says the same rather than "depth reached".
- Our own 400-page bound on a single video is named as ours, so a video that hits it is never
reported as YouTube running out of comments.
- A video cut off by the run clock mid-walk now carries its two numbers on every path. One of
those paths still read as though nothing had been collected — on a video that had already delivered
959 charged comments. It now says how many it collected, how many were left, and which setting to
raise. The same is true when a feed stops answering part-way through a video: the row no longer
says there was no result fee over rows that were charged.
Your inputs, your events and your prices are unchanged, and nothing you have already collected needs
re-running.
- A video the run stopped part-way through now says how far it got. The uncharged row for a
video whose walk was ended by one of your own limits used to read as though nothing had been
collected from it — on a video that had already delivered 1,140 comments. It now says how many
comments it collected from that video, how many were left behind, and which setting to raise to
go back for them.
- Every delivered row now carries
chargedEvents, naming the event it was billed as, so one
row can be checked against your invoice on its own instead of by counting the whole dataset. The
column was already described in the output schema; from this build it is in the row.
Your inputs, your events and your prices are unchanged, and nothing you have already collected needs
re-running.
First public build.
- A YouTube video link returns its comments as rows: the comment's own stable id, the text, the
author with their channel id and YouTube's verified, uploader and artist badges, the like count,
the reply count, the pinned and hearted-by-the-uploader flags, and the video and channel the row
came from. Watch links,
youtu.be links, Shorts, live, embed and /v/ URLs and a bare
11-character id are all read.
- A channel link is a first-class input. A handle,
/@handle, /channel/UC…, /c/… or
/user/… returns the comments under that channel's newest uploads, videosPerChannel of them.
No other comments scraper on this store takes a channel link.
- Replies are rows at the same price, folded under the comment they answer. With
includeReplies on (the default), a comment with a thread brings that thread, each reply carrying
isReply and parentCommentId, counted inside your own per-video limit. There is no second
event and no surcharge for a thread.
- Your limit is exact and it is per video.
resultsLimit (default 100) is what each link
returns; maxItems (default 1,000) caps the whole run. maxComments, commentsPerVideo, limit
and count — the names other comment scrapers use — are read as resultsLimit, with one
uncharged note row saying so, rather than being ignored.
- Two orders, and they are YouTube's own two:
sort: top (the default) and sort: newest. An
order we do not know is refused on an uncharged row naming the two that work, never swapped for
something else.
- How deep a sort goes is documented, because it is YouTube's ceiling and not ours — and it is
approximate. Walked to exhaustion twice, a day apart:
top stops at about 1,200 comments per
video whatever the video's size (around 1,220 on a 2.46-million-comment video, around 1,130-1,140
on a 5,171-comment one — 1,130 on the first walk and 1,137 on the second, both a clean end), while
newest was still serving at 3,000 with more to come. YouTube decides when to stop serving, so
the figure moves a little between runs and the README states a range rather than a number. When
the pages run out before your limit, the run leaves an uncharged depth_reached row saying how
far that order reached and naming newest as the deeper door.
- Dates are shipped at the precision YouTube actually publishes. The surface carries a relative
label and no timestamp, so the row carries
publishedText verbatim, a derived publishedAt and
publishedAtPrecision (minute to year) saying how precise that derivation is. A comment
labelled "1 year ago" is never returned as a millisecond stamp.
- Rounded like counts are named, not hidden. Above 999 YouTube publishes "314K" rather than a
number, so the row carries
likeCount, likeCountText and likeCountIsApproximate.
- A video with nothing to give answers with a typed, uncharged row, and the answers are told
apart:
comments_disabled (the uploader turned comments off — read from the video's own page
before any comment page is requested), no_comments, removed_or_unavailable,
private_or_members_only, age_restricted, region_blocked, and the channel door's
channel_not_found, not_a_channel and channel_no_videos. None of them is charged.
- Account memory so a schedule pays only for what is new: every comment delivered to your account is
remembered in the key-value store
yt-comments-account and skipped on the next run, with
includeSeen to get them back anyway, uncharged, and resumeFromDatasetId to carry the memory
across accounts.
- A run clock (
maxRunSeconds, default 600) and maxTotalChargeUsd as a hard ceiling. A cap or the
clock ends the collecting, never the delivering: comments already in hand are always written out,
and the last row of every run is a receipt that reconciles the invoice from the dataset itself.
- Click Start with nothing set and the run returns a small built-in sample: three rows cut from
a public video's own comment page, one of them a reply folded under its parent, so the columns can
be read before a big run is planned. It is built in rather than fetched — nothing is collected and
nothing is charged. The comment text and the video are as they were published; the commenter names
in the sample are anonymised, and the rows say so.
- One charged event,
comment, billed only when a comment lands in your dataset. No start fee and
no result fee.