YouTube Transcript & Subtitle Scraper avatar

YouTube Transcript & Subtitle Scraper

Pricing

from $1.00 / 1,000 transcript fetcheds

Go to Apify Store
YouTube Transcript & Subtitle Scraper

YouTube Transcript & Subtitle Scraper

Extract transcripts and subtitles from YouTube videos in bulk using video, playlist, channel URLs, or keyword search. Returns timed transcript segments, plain text, SRT, and WebVTT subtitle files, with optional auto-translation to other languages.

Pricing

from $1.00 / 1,000 transcript fetcheds

Rating

0.0

(0)

Developer

Abot API

Abot API

Maintained by Community

Actor stats

0

Bookmarked

40

Total users

7

Monthly active users

7 days ago

Last modified

Share

YouTube Transcript & Subtitles Scraper

Pull transcripts and subtitles for YouTube videos in bulk. Give it video URLs, playlist URLs, channel URLs, or just keywords to search YouTube, and it returns each video's transcript as timed segments, as plain text, and as ready-to-use SRT and WebVTT subtitle files. It can also auto-translate transcripts into another language.

Why this scraper

  • Four ways in: direct video URLs or IDs, playlist URLs, channel URLs (@handle, /channel/UC..., /c/..., /user/...), and keyword searches, all in one run.
  • Every output record carries the transcript three ways: timed segments, a single plain-text transcript field, and srt plus vtt strings you can save straight to disk.
  • Language control: list your preferred languages in priority order, or translate the result into any language YouTube supports.
  • Handles both human-written captions and auto-generated ones, and falls back to whatever the video offers when your preferred language is not available.
  • Concurrent fetching with automatic connection rotation, so large playlists and channels move quickly.
  • Per-source time windows: trim each video's transcript to a specific second range (and only pay for the trimmed text).
  • Pay only for what you get: failed, captions-disabled, or incremental-mode-suppressed videos are not billed.
  • Resume an interrupted large pull, or schedule the actor to run daily/weekly and get only what's NEW, UPDATED, REAPPEARED, or EXPIRED since last time.

Data you get

Per video (one dataset record). Values below are illustrative placeholders, not from a live video.

FieldExample
videoIdEXAMPLE_ID1
videoUrlhttps://www.youtube.com/watch?v=EXAMPLE_ID1
videoTitleSample Video Title
channelNameSample Channel
channelUrlhttps://www.youtube.com/@SampleChannel
durationSeconds213
sourcepython tutorial
sourceTypesearch (one of video, playlist, channel, search)
languageEnglish
languageCodeen
isGeneratedfalse
isTranslatedfalse
translatedTonull
charCount5234
segmentCount142
transcript"Hello and welcome to this sample transcript ..."
segments[{ "text": "Hello and welcome", "start": 0.0, "duration": 1.84 }, ...]
srt"1\n00:00:00,000 --> 00:00:01,840\nHello and welcome\n\n2\n..."
vtt"WEBVTT\n\n00:00:00.000 --> 00:00:01.840\nHello and welcome\n\n..."
trimmedStart0 (start of the time window applied; 0 = no trim)
trimmedDuration0 (length of the time window applied; 0 = no trim)
successtrue
errornull (a short message when a transcript could not be fetched)
fetchedAt2026-07-30T08:13:20.560058+00:00 (when this row was fetched, ISO 8601 UTC)
changeTypeNEW (incremental mode only — NEW | UPDATED | UNCHANGED | REAPPEARED | EXPIRED)
changedFields[] (incremental mode only — field names that changed since the last run, on UPDATED rows)
firstSeenAt2026-07-30T08:13:20Z (incremental mode only)
lastSeenAt2026-07-30T08:13:20Z (incremental mode only)

changeType/changedFields/firstSeenAt/lastSeenAt are only added when incrementalMode is on; a normal run's rows keep the shape above without them.

How to use

Pick a mode:

  • mode: "url" reads videoUrls (paste video, playlist, or channel URLs).
  • mode: "search" reads searchQueries (find videos by keyword).

The other field is ignored. Optionally set startSec and durationSec to trim every video's transcript to the same time window (durationSec: 0 means "until end of video").

Fetch one video's transcript:

{
"mode": "url",
"videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
"languages": ["en"],
"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}

Several videos at once (URLs or bare IDs both work):

{
"mode": "url",
"videoUrls": [
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"https://youtu.be/dQw4w9WgXcQ",
"dQw4w9WgXcQ"
],
"languages": ["en", "en-US"],
"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}

Search YouTube by keyword and transcribe the top results:

{
"mode": "search",
"searchQueries": ["langgraph tutorial", "apify actor development"],
"maxVideosPerSource": 5,
"languages": ["en"],
"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}

Expand a playlist and a channel, capped per source, and translate everything to English:

{
"mode": "url",
"videoUrls": [
"https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxx",
"https://www.youtube.com/@SomeChannel"
],
"maxVideosPerSource": 25,
"maxVideos": 100,
"languages": ["en"],
"translateToLanguage": "en",
"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}

Trim every video's transcript to seconds 30 to 60:

{
"mode": "url",
"videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
"startSec": 30,
"durationSec": 30,
"languages": ["en"],
"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}

The trimmed transcript, segments, srt, and vtt only contain snippets inside the window, and the transcript billing event counts only the trimmed characters.

Resume & recurring updates

Two different, complementary things:

  • resumeFromRunId continues one interrupted crawl. Paste a previous run ID or dataset ID; this run skips videos it already collected there instead of re-fetching (and re-billing) them.
  • incrementalMode is for scheduling this actor on the same videoUrls/searchQueries setup again and again (daily, weekly) and getting only what changed. The actor remembers its own previous run of that setup, keyed automatically from mode + URLs/queries + language settings (or a stateKey you name yourself). Every video is classified NEW, UPDATED, UNCHANGED, REAPPEARED, or EXPIRED; by default only NEW/UPDATED/REAPPEARED are returned (and billed) — turn on emitUnchanged or emitExpired to also get those.

Schedule daily monitoring of a search, returning only what changed:

{
"mode": "search",
"searchQueries": ["langgraph tutorial"],
"maxVideosPerSource": 20,
"incrementalMode": true,
"languages": ["en"],
"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}

Continue a large playlist pull that got interrupted:

{
"mode": "url",
"videoUrls": ["https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxx"],
"resumeFromRunId": "<previous run ID>",
"maxVideosPerSource": 500,
"languages": ["en"],
"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}

Notes:

  • EXPIRED is only ever produced in mode: search, and only once a run genuinely scanned every search query to its end (not capped by maxVideos, not a resumeFromRunId run, not a search that may have returned more results than maxVideosPerSource asked for). In mode: url the tracked set is whatever URLs you pasted, so a video missing this run simply wasn't pasted — EXPIRED can never fire there.
  • An EXPIRED row is synthesized from the last known data, not re-fetched, so it is never billed the transcript event.
  • A suppressed UNCHANGED video still has to be fetched (to compute its charCount fingerprint) but is never pushed or billed — only NEW/UPDATED/REAPPEARED rows (and UNCHANGED/EXPIRED if you opt in) cost anything.

Input parameters

ParameterTypeDefaultDescription
modestring enum"url"One of "url" (read videoUrls) or "search" (read searchQueries). The other field is ignored.
videoUrlsarray of strings[]Used when mode = url. Watch URLs, youtu.be URLs, shorts URLs, playlist URLs, channel URLs (@handle, /channel/, /c/, /user/), or 11-character video IDs. Playlists and channels are expanded to their videos.
searchQueriesarray of strings[]Used when mode = search. Free-form keywords. Top results per query are fetched.
startSecinteger0Trim every transcript to start at this second. 0 keeps from the beginning.
durationSecinteger0Trim every transcript to this many seconds from startSec. 0 keeps until the end of each video.
maxVideosPerSourceinteger10How many videos to take from each playlist, channel, or search query.
maxVideosinteger0Hard cap on the total number of videos across all sources. 0 means no overall cap.
languagesarray of strings["en"]Preferred transcript language codes in priority order. The first available one is used; if none are available, any transcript the video offers is returned.
translateToLanguagestring""Optional language code to translate the transcript into using YouTube's auto-translation. Empty keeps the original language.
preserveFormattingbooleanfalseKeep inline formatting tags (such as italics) in the transcript text instead of stripping them.
resumeFromRunIdstring""Continue one interrupted run: paste a previous run ID or dataset ID and this run skips videos already collected there.
incrementalModebooleanfalseTurn on for recurring monitoring of the same setup. See Resume & recurring updates.
emitUnchangedbooleanfalseIncremental mode only. Also return (and bill) videos that haven't changed since the last run, marked UNCHANGED.
emitExpiredbooleanfalseIncremental mode only, mode: search only. Also return videos no longer found, marked EXPIRED. Never billed.
stateKeystring""Incremental mode only. Optional name for this monitoring campaign's saved state. Leave empty to derive one automatically from the setup.
proxyConfigurationobjectApify Proxy, RESIDENTIALProxy settings. YouTube blocks most datacenter / cloud IPs from fetching transcripts, so the RESIDENTIAL group is strongly recommended.

Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape — the Apify dataset is never changed.

What gets written to the connector: a condensed, human-readable summary of each record — not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.

  1. Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
  2. Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
  3. For Notion, also set notionParentPageUrl to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

Output example

Sample shape: values are illustrative placeholders, not from a live video.

{
"videoId": "EXAMPLE_ID1",
"videoUrl": "https://www.youtube.com/watch?v=EXAMPLE_ID1",
"videoTitle": "Sample Video Title",
"channelName": "Sample Channel",
"channelUrl": "https://www.youtube.com/@SampleChannel",
"durationSeconds": 213,
"source": "https://www.youtube.com/watch?v=EXAMPLE_ID1",
"sourceType": "video",
"language": "English",
"languageCode": "en",
"isGenerated": false,
"isTranslated": false,
"translatedTo": null,
"charCount": 5234,
"segmentCount": 142,
"transcript": "Hello and welcome to this sample transcript.\nThis is the second line of the transcript.\n...",
"segments": [
{ "text": "Hello and welcome to this sample transcript.", "start": 0.0, "duration": 2.32 },
{ "text": "This is the second line of the transcript.", "start": 2.32, "duration": 2.08 }
],
"srt": "1\n00:00:00,000 --> 00:00:02,320\nHello and welcome to this sample transcript.\n\n2\n00:00:02,320 --> 00:00:04,400\nThis is the second line of the transcript.\n",
"vtt": "WEBVTT\n\n00:00:00.000 --> 00:00:02.320\nHello and welcome to this sample transcript.\n\n00:00:02.320 --> 00:00:04.400\nThis is the second line of the transcript.\n",
"success": true,
"error": null,
"fetchedAt": "2026-07-30T08:13:20.560058+00:00"
}

With incrementalMode on, rows also carry changeType, changedFields, firstSeenAt, and lastSeenAt — see Resume & recurring updates.

Billing

You are charged once per run, plus a small amount per keyword search that was successfully resolved (URL inputs are not charged for resolution — and a resolved search is billed even if every video it finds turns out unchanged, since the search itself still ran), plus a length-based unit per fetched transcript. One transcript unit covers roughly 4,000 characters of transcript text, about 1,000 LLM tokens. A short clip is about one unit, a 1-hour podcast around twelve, and a 3-hour video around thirty-five. Videos whose captions are disabled, unavailable, or could not be fetched are not billed. If you trim the transcript with startSec and durationSec, you only pay for the trimmed text. In incrementalMode, a video suppressed as UNCHANGED is not billed either, even though it still had to be fetched to confirm nothing changed — and an EXPIRED row is never billed, since nothing was re-fetched for it. Exact amounts are shown on this actor's Store page.

Plan requirement

Works on any Apify plan, but YouTube blocks most datacenter and cloud IP ranges from fetching transcripts. For reliable results use Apify Proxy with the RESIDENTIAL group, which is available on the Starter plan and higher. On the free plan, expect many videos to return an error; the run will log a notice and add a record explaining the upgrade path.