YouTube Channel Videos Transcript Scraper — Bulk Video to Text avatar

YouTube Channel Videos Transcript Scraper — Bulk Video to Text

Pricing

from $1.20 / 1,000 transcripts

Go to Apify Store
YouTube Channel Videos Transcript Scraper — Bulk Video to Text

YouTube Channel Videos Transcript Scraper — Bulk Video to Text

YouTube channel videos to text: newest 50 per channel by default, up to 5,000 — videos, Shorts and live VODs; captions first, speech-to-text when none at $0.008 per started minute. JSON, text, SRT or VTT. From $1.20 per 1,000 transcripts, no per-channel fee. Never charged for a video we can't read.

Pricing

from $1.20 / 1,000 transcripts

Rating

0.0

(0)

Developer

Steadyfetch Team

Steadyfetch Team

Maintained by Community

Actor stats

0

Bookmarked

9

Total users

8

Monthly active users

3 hours ago

Last modified

Categories

Share

Paste a channel, get every video's transcript in one run. Videos, Shorts and live VODs, de-duplicated. Captions first, built-in speech-to-text when a video has none — so caption-less videos still return real text. No chaining, no per-channel fee, never charged for a video we can't transcribe — or for one you already have, so a weekly poll pays only for what is new.

Using an AI agent? Pin this actor in Apify's MCP server with one link: https://mcp.apify.com?tools=steadyfetch/youtube-channel-transcripts

  • Actor id: steadyfetch/youtube-channel-transcripts
  • Input: { "channels": ["https://www.youtube.com/@mkbhd"] } — the one field you have to set.
  • Cap the bill: set maxTotalChargeUsd on the run (a run option, not Actor input), e.g. 0.50 — the run stops when it reaches it.

Just want to see it work? Click Start with nothing set and the run transcribes one real channel (@jawed, its single public video, "Me at the zoo"), charged like any run at your plan's per-transcript price. Set only settings — a different Output format, a caption language, speech-to-text off, a limit — and leave Channels empty, and that same sample runs under your settings, charged like any run. Put your own channel URL, @handle or channel ID in Channels for your own run.

YouTube Channel Transcript Scraper input form in the Apify console: the Channels list prefilled with youtube.com/@mkbhd, above the collapsed how-much, transcript-options and cost-control sections

Issues answered in a couple of hours. Unofficial: steadyfetch is not affiliated with, endorsed by, or sponsored by YouTube or Google. "YouTube" is a trademark of Google LLC, used here only to say what this actor reads.

There is no per-channel fee — you only pay for transcripts that are delivered. Every row carries charged and statusReason, so you can reconcile the invoice from the dataset itself. Only rows with charged: true were billed. If your Maximum cost per run is reached while a video's transcript is already in hand, that transcript still ships in full with charged: false and a statusReason naming the cap — your cap is never exceeded, and work already done is never thrown away.


What a row looks like

Dataset table of a real run: one row — @jawed’s “Me at the zoo”, manual captions, English, 19 seconds, with the transcript text

Real output, unedited apart from trimming the transcript and segment list:

{
"status": "ok",
"charged": true,
"statusReason": null,
"inputChannel": "@mkbhd",
"channelHandle": "@mkbhd",
"positionInChannel": 0,
"videosFound": 25,
"videoId": "ngPkbaZliaU",
"url": "https://www.youtube.com/watch?v=ngPkbaZliaU",
"title": "The Truth About the Bezelless Concept Phones",
"channelName": "Marques Brownlee",
"channelId": "UCBJycsmduvYEL83R_U4JriQ",
"durationSeconds": 389,
"viewCount": 2522420,
"isLiveContent": false,
"language": "en",
"source": "captions",
"captionKind": "manual",
"text": "Do you remember back in the day when… screen-to-body ratio was one of those headlining numbers…",
"segments": [
{ "start": 2.593, "end": 4.781, "text": "Do you remember back in the day when..." },
{ "start": 4.863, "end": 7.295, "text": "screen-to-body ratio was one of those, like," }
],
"srt": null,
"vtt": null,
"chargeEvents": { "transcript": 1, "speechMinutes": 0 }
}
fieldnotes
inputChannel · channelHandle · positionInChannel · videosFoundwhich channel this came from, and where in it — join rows back to the channel you pasted
text · segments[{start,end,text}]the transcript, and the same text with timestamps in seconds
srt · vttfilled only when you ask for that format; otherwise null
sourcecaptions or speech_ai — how this transcript was produced
captionKindmanual (uploaded by the channel) or auto (YouTube's own auto-captions); null on the speech route
title · channelName · channelId · durationSeconds · viewCount · isLiveContentas YouTube reports them
charged · statusReason · chargeEventsthe reconciliation trio
repeat · firstSeenAt · firstSeenRunIdtrue when this account already had this transcript: it was handed back from the run named here, and nothing was charged for it
retryableon a row that did not deliver: true means YouTube refused us this time and the same input is worth running again, false means the answer will not change. It always agrees with the note in statusReason, so code can branch on the column instead of parsing the sentence

The run summary (in the key-value store) carries a per-channel breakdown: videos found vs transcribed, and the reason enumeration stopped (completed, maxVideos, …).

Browse the full live example dataset → — one real @mkbhd run, four transcripts as delivered: a 31-minute video (30,883 characters), a Pixel impressions video, a phone review and a foldable review, each with its own charged and chargeEvents line.

Agent / API paste-block

Actor: steadyfetch/youtube-channel-transcripts
Required: channels (array of channel URLs, @handles or channel IDs — a single video link works too)
Optional: maxVideos (integer, default 50, up to 5,000 — newest N per channel)
maxItems (integer, default 500, up to 5,000 — cap for the WHOLE run)
language (string, e.g. "en", "es", "pt-BR" — preferred caption track)
format (json | text | srt | vtt, default json)
enableSpeechFallback (boolean, default true — off = captions only, no speech minutes)
maxSpeechMinutes (integer, default 60 — hard cap for the whole run)
Charges: transcript once per delivered transcript, captions or speech-to-text
speech_minute per started minute, only when speech-to-text actually ran
Build spec: https://apify.com/steadyfetch/youtube-channel-transcripts/api
Token: https://console.apify.com/settings/integrations
MCP: pinned in one link — see the top of this page
curl -X POST "https://api.apify.com/v2/acts/steadyfetch~youtube-channel-transcripts/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"channels":["https://www.youtube.com/@mkbhd"],"maxVideos":25,"format":"srt"}'

Calling from an agent or MCP client: every optional field accepts an explicit null and reads it as "use the default", so a template that renders one body per run and sends null for everything it left unset still runs. format included. The one exception is channels: it is required and needs a real value. An unrecognised format does not fail the run either — nothing is transcribed, nothing is charged, and one row names the formats that exist so you can re-run with one of them.

Sending the same list under a sibling actor's field name works too: videoUrls and urls are read as this actor's list, and one uncharged note row tells you the field is called channels here. Channel URLs, @handles, channel IDs and single video links are all accepted whichever of the three names you send them under, so a body written for another transcript actor runs here unchanged.

MCP: one link pins this actor in Claude, Cursor, or any MCP client — the link is at the top of this page — or ask Apify's MCP server for "youtube channel transcripts".


What you can paste

A channel URL (youtube.com/@handle, /channel/UC…, /c/…, /user/…), a bare @handle, a bare channel ID, or a full channel URL with any trailing tab. Casing in the link does not matter. A single video link works too — it transcribes just that video. Videos, Shorts and live recordings are all enumerated and de-duplicated into one list.

maxVideos caps how many of a channel's newest videos are transcribed (default 50, up to 5,000 per channel). Raise it — and your Maximum cost per run — to transcribe a whole channel.

Asking past a cap is never a refusal. maxVideos and maxItems take up to 5,000 each and maxSpeechMinutes up to 100,000; ask for more and the run still starts, continues at the ceiling, and ships one uncharged row saying what you asked for and what bound it.

maxItems is the second cap, and it applies to the whole run rather than to each channel (default 500). Per-channel caps multiply — ten channels at 50 is 500 transcripts — so this is the one number that bounds what a multi-channel run can bill you. Channel reports, skipped videos and uncharged misses carry no result fee and never count against it; when the run stops on this limit the status line says so and names how many videos were left.

newVideosOnly (New videos only, default off) is the switch for a scheduled run. Off, a video this account already has comes back from the earlier run marked repeat: true with nothing charged for it, so a re-run is a complete, current archive. On, those videos are left out of the dataset altogether — nothing fetched, nothing charged, no row — and the status line says how many were skipped, so a Sheet or a webhook receives one row per new upload and nothing else. It needs the account memory to work, so a run whose API token cannot open key-value stores stops before listing anything and says which permission to grant, rather than charging you for the back catalogue you asked it to skip. See the recipe below.

List a channel's videos without transcripts

Send transcripts: false and the run stops at the listing: one row per video, no transcript, and no transcript or speech-to-text minute charged. There is no charged event for a channel's video listing, so a listing run bills neither of this actor's two events — you pay only the Apify platform usage every run spends.

{ "channels": ["https://www.youtube.com/@mkbhd"], "maxVideos": 50, "transcripts": false }

Each row carries what YouTube's own channel grid says about the video:

{
"status": "video_listing",
"charged": false,
"inputChannel": "https://www.youtube.com/@mkbhd",
"channelHandle": "@mkbhd",
"channelId": "UCBJycsmduvYEL83R_U4JriQ",
"positionInChannel": 0,
"videosFound": 5,
"videoId": "6D__H_DO2Xk",
"url": "https://www.youtube.com/watch?v=6D__H_DO2Xk",
"title": "iPhone Duo: What We Missed!",
"durationSeconds": 637,
"viewsText": "7.8M views",
"publishedText": "1 day ago",
"thumbnail": "https://i.ytimg.com/vi/6D__H_DO2Xk/hq720.jpg"
}

Two numbers are deliberately missing, and that is the honest answer. A channel grid gives rounded words — "7.8M views", "1 day ago" — and nothing more exact. So the row ships those words as viewsText and publishedText and does not ship a viewCount of 7,800,000 or an upload timestamp derived from "1 day ago": both would be a precision YouTube never gave us. When the grid does print an exact count the row carries viewCount as well. durationSeconds and thumbnail are read straight off the grid and are exact.

How often does this change? As often as the channel uploads — for most channels that is weekly, for a news or daily-vlog channel it is daily, and for a dormant channel it is never. View and age text move on every listing. Nothing is remembered between runs in this mode: a listing is the answer to "what is on this channel now", so every run re-lists the channel in full and hands you the current state. That is the opposite of the transcript mode, where a video you already have comes back uncharged instead of being fetched again.

maxVideos and maxItems bound a listing run exactly as they bound a transcript run. An AI agent that guesses mode: "videos", videosOnly: true or skipTranscripts: true gets the same listing, plus one uncharged row naming the field this actor actually declares.

What you are charged for

Pricing: from $1.20/1,000 transcripts. Two events, and nothing else — listing the channel adds no separate fee:

  • Transcript — once per video that returns real text, from captions or from speech-to-text.
  • Nothing at all — when you send transcripts: false. The run lists the channel's videos and stops there, so neither event fires. See the section above.
  • Speech-to-text minute — per started minute, and only when a video had no usable captions so speech-to-text had to run. A captioned video never triggers it, and a caption-less video too long for the speech route (see audio_too_long_for_speech below) is reported uncharged instead of being charged for a partial transcript.

What can fail, and what it costs you: nothing. The two most common non-deliveries are no_speech (the audio is music or silence, so there is no transcript to sell) and blocked_retry (YouTube challenged or throttled the fetch, or was still processing the video). Both come back as a row with charged: false naming the reason — as do no_captions, private_or_members_only, removed_or_unavailable, age_restricted, region_blocked and live_no_transcript_yet. You pay for delivered transcripts and nothing else.

Switch "Use speech-to-text when a video has no captions" off and you will never be charged a speech-to-text minute; caption-less videos come back as uncharged rows instead. Max videos and Max speech-to-text minutes are hard stops: the run finishes successfully and each skipped row names the limit that stopped it.

Run it weekly — the whole channel stays current, you pay for the new uploads

Save the channel list as a Task, put it on an Apify Schedule (weekly for most channels, daily for a channel that uploads daily), and add an integration on run succeeded. Every run re-lists the channel, hands back the videos your account already has (repeat: true, not charged — they do not use up your video caps either), and transcribes only the new uploads. Turn New videos only on and the dataset holds just those, so a Sheet or a webhook receives one row per new video and nothing else.

The check happens before anything is bought: no player call, no caption fetch, no speech-to-text minute. The same video reached two ways is still one charge — pasted as a link today, listed by its channel tomorrow. A handed-back row carries repeat: true, charged: false, firstSeenAt (when you first got it) and firstSeenRunId (which run), and statusReason says the same in words; with New videos only on there is no row at all and the status line carries the count instead.

The memory lives in your own account, in a key-value store called yt-channel-transcripts-account on your Storage tab. Delete it to start over and be charged again. Transcripts drop out of it after 90 days on their own. If it cannot be read on a given run, the run still delivers — it charges as it always did and the status line says the check was unavailable, so you know that run could have billed a repeat. With New videos only on, that run stops before it lists anything instead, and says so on an uncharged row: the switch is a promise about that record, and we would rather run nothing than charge you for videos you asked us to skip.

One thing can switch the memory off without you meaning to: a scoped API token. The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission (or Actor runs set to Full access) — set under Settings → API & Integrations (Write is what lets the run remember what it delivered; without it every run pays again). Without it the run says so on its status line and in an uncharged first row, and it cannot skip what you already have, so a scheduled poll transcribes and charges the whole back catalogue again. A run started from the console, or with a full-access token, is unaffected.

When a video (or channel) is not charged

statuswhat happenedtemporary?
channel_not_foundthe channel handle / ID / URL did not resolveno
no_videos_foundthe channel has no public videos, Shorts or live VODsno
resolve_failed · enumerate_failedthe channel could not be looked up or listed right nowyes — re-run
channel_region_blockedYouTube does not serve this channel to any of the locations the run went out from (it tries three) — a limit on where we went out, not a verdict on the channel. Runs can leave from different locations, so another one may be allowed to list ityes — re-run
no_speechthe audio is music or silence — there is no transcript to sellno
no_captionsno captions, and you switched speech-to-text off for this runno
asr_unavailableour speech-to-text service refused this actor's access mid-run — that is on us, not you. Captioned videos still delivered; caption-less ones came back unchargedyes — try again later
audio_too_long_for_speechno captions, and the video is too long for the speech-to-text route — YouTube does not release enough of its audio for a complete transcript, and we never sell a partial one as whole. Captioned videos are unaffected at any lengthno
private_or_members_onlyprivate or members-onlyno
removed_or_unavailableYouTube says the video is gone; its own words are in the rowno
age_restrictedYouTube requires a signed-in, age-verified accountno
region_blockedthe uploader has not published it in the country we fetched fromno
live_no_transcript_yeta live stream — no transcript exists until it endsre-run after it ends
blocked_retryYouTube challenged, throttled or was still processingyes — re-run
skipped_budget · skipped_deadline · skipped_speech_capa limit stopped it before the video was fetched, and the row names which one: your maximum cost per run, the run timeout, or the speech-minute cap (a transcript already in hand when the cost cap is reached is delivered instead, uncharged)yes
input_errorthe paste was not a channel or videofix and re-run
sample_noteyou set settings but no channels, so the sample ran under them — the row names what you set, and is not charged
video_listingyou sent transcripts: false, so the video was listed and not transcribed — id, URL, title, duration, thumbnail and the grid's own view and age text, nothing charged

A temporary problem is never reported as a permanent one. This actor may fail when the platform changes things — failed items are never charged.

FAQ

How do I download every transcript from a YouTube channel? Paste the channel URL or @handle and run it. No YouTube API key, no cookies, no sign-in.

Does it include Shorts and live streams? Yes — the Videos, Shorts and Live tabs are all enumerated and merged, de-duplicated by video ID.

Can I transcribe just the newest few videos? Set maxVideos. Videos are taken newest-first.

What if a video has no captions? Speech-to-text runs on the audio and you get real text, marked source: "speech_ai". If the audio has no speech at all, the row comes back no_speech and uncharged. The speech route works on short videos only: past a few minutes YouTube stops releasing the audio to anything but its own player, so a long caption-less video comes back audio_too_long_for_speech and uncharged rather than half-transcribed. Shorts are unaffected, and so is every video that has captions — the large majority of a channel, including nearly every spoken upload — at any length.

Can I get SRT or VTT subtitles? Set format to srt or vtt and each row carries a ready-to-save subtitle string.

Can I use this through an MCP server? Yes. It is a standard Apify actor, so any MCP client that can call Apify actors can call it.

Why does a run cost more than the transcripts? Apify bills platform usage (compute and proxy) for what a run actually consumes, separately from these events. Maximum cost per run is the ceiling that covers both.


Steadyfetch YouTube suite

Same transcript engine, different way in. All-inclusive pay per event, no start fee, charged only on delivery.

What you pasteActor
Video URLs or IDsYouTube Transcript API & Scraper — YouTube Subtitles, Speech AI
A channel URL, @handle or channel IDthis actor

The rest of the steadyfetch shelf — same contract everywhere: all-inclusive pay per event, no start fee, charged only on delivery.

FamilyActors
Ad creative intelligenceFacebook · Google Ads video · TikTok · LinkedIn · Google Ads text & OCR
Trends & keywordsGoogle Trends · Trends Now · Breakout keywords · Autocomplete keywords · Keyword volume & CPC · Social trends
YouTube transcriptsYouTube videos · YouTube channels
InstagramReel transcripts · Profile posts
JobsIndeed · Career sites by domain · Glassdoor · Multi-board · Google Jobs
AmazonProducts · Search · Bestsellers · Sellers
Any media fileSpeech to Text · any link or file

Feedback & support

Found an issue? Open it on the Issues tab — we usually reply within a couple of hours, always within a day.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.