YouTube Transcript Extractor: Timed Captions avatar

YouTube Transcript Extractor: Timed Captions

Pricing

Pay per usage

Go to Apify Store
YouTube Transcript Extractor: Timed Captions

YouTube Transcript Extractor: Timed Captions

Extracts timed captions from YouTube videos through the ANDROID player endpoint behind rotating residential proxies. Returns manual or auto-generated tracks with start times, language, full text and video details, and retries bot checks on a new IP for every video.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

luochengfengqi

luochengfengqi

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

YouTube Transcript Extractor

Reads the caption track of YouTube videos and returns it as structured data: the full text, the timed segments, the language, whether the track was written by a person or generated by speech recognition, and basic details of the video. Give it video URLs or 11-character video IDs, get one dataset record per video.

The Actor does not use a browser. For every video it calls the same player endpoint that the YouTube Android app uses, picks the caption track that matches your language preference, downloads the timed text and parses it. Requests go through Apify residential proxies, and a request that YouTube answers with a bot check is repeated on a new IP address.

What you get

  • Full transcript as one clean text string, plus an optional array of timed segments (start, duration, text).
  • The track that was returned: language code, language name and kind (manual or auto-generated).
  • The list of all caption tracks the video offers, so you can see which languages exist.
  • Video details from the player response: title, channel name and ID, length, view count, keywords and live status.
  • A status on every record, so a video without captions, a deleted video or a blocked request is a visible row and not a silent gap.
  • A run summary with status counts, attempts, language and track-kind counts, in the key-value store record SUMMARY.

Only records with status OK count as transcripts. See the pricing section for what a run costs.

How it works

  1. Every input entry is parsed into a video ID. Accepted are bare IDs and URLs of the forms watch?v=, youtu.be/, /shorts/, /embed/, /live/ and music.youtube.com. Playlists, channels and search URLs become INVALID_INPUT records. Duplicates are skipped and counted.
  2. For each video the Actor sends a POST request to https://www.youtube.com/youtubei/v1/player as the ANDROID client (version 20.10.38) through a residential proxy session.
  3. The response is classified. A playable video with caption tracks continues. A video that does not exist, is private, age-restricted or an unavailable live recording ends as UNAVAILABLE. A playable video without tracks ends as NO_CAPTIONS. A "Sign in to confirm you are not a bot" answer is treated as a blocked request and retried.
  4. One track is chosen from the list: your languages are tried in order, an exact language code beats a regional variant, and a manual track beats an auto-generated one unless you turn that off.
  5. The caption URL from the player response is downloaded through the same proxy session and parsed from the YouTube timed text format 3 (XML) into segments. HTML entities are decoded once, line breaks inside a caption are kept as spaces in the joined text, and emoji characters are removed.
  6. If any step of an attempt fails for a transient reason (bot check, network error, HTTP error, empty caption body, request stalled for more than 22 seconds), the attempt is repeated on a new proxy session after a short pause, up to maxAttempts times. Answers that will not change on retry (unavailable video, no captions, no matching language) are returned immediately.
  7. The record is written to the dataset. Under pay-per-event pricing, a record with status OK is charged as one transcript-extracted event together with the dataset write, and records with any other status are never charged.

Quick start

  1. Open the Input tab and paste video URLs or IDs into Videos. The prefilled input contains three public videos.
  2. Keep the proxy configuration on Apify Proxy with the RESIDENTIAL group.
  3. Start the Actor. A run with three videos takes a few seconds.
  4. Open the Output tab. The dataset overview shows one row per video. Open SUMMARY in the key-value store for the totals.

Input Parameters

ParameterTypeDefaultDescription
videosarray of strings, requiredthree sample videosYouTube video URLs or 11-character IDs. Duplicates are skipped.
languagesarray of strings["en"]Language codes in order of preference, for example en, de, pt-BR. A base code such as es also matches regional tracks such as es-419. An exact code wins over a regional variant. Case-insensitive.
fallbackToAnyLanguagebooleantrueIf none of the preferred languages exists, return the first allowed track. With false the record becomes NO_CAPTIONS and lists the languages that exist.
includeAutoGeneratedbooleantrueAllow tracks generated by speech recognition. With false, videos that only have such tracks become NO_CAPTIONS.
preferManualCaptionsbooleantrueIf a manual and an auto-generated track exist for the same language, return the manual one.
includeSegmentsbooleantrueAdd the segments array. With false only text and the statistics are returned, which keeps the dataset smaller.
maxConcurrencyinteger, 1 to 205Number of videos processed at the same time.
maxAttemptsinteger, 1 to 53Attempts per video before it is reported as BLOCKED or ERROR. Every attempt uses a new residential IP.
proxyConfigurationobjectApify Proxy, group RESIDENTIAL, country USResidential proxies are required. See the limitations section.

Example input

{
"videos": [
"jNQXAC9IVRw",
"https://youtu.be/dQw4w9WgXcQ",
"https://www.youtube.com/watch?v=UF8uR6Z6KLc"
],
"languages": ["de", "en"],
"fallbackToAnyLanguage": true,
"includeAutoGenerated": true,
"includeSegments": true,
"maxConcurrency": 5,
"maxAttempts": 3,
"proxyConfiguration": {
"useApifyProxy": true,
"apifyProxyGroups": ["RESIDENTIAL"],
"apifyProxyCountry": "US"
}
}

Output Schema

The default dataset holds one record per input entry that is a video, plus one INVALID_INPUT record per entry that is not. Every record has all fields listed below. Fields that do not apply are null (or an empty array for list fields), so the columns stay stable when you export to CSV or Excel.

Field Reference

FieldTypeDescription
inputstringThe entry exactly as it was provided.
videoIdstring or nullThe 11-character video ID. null for INVALID_INPUT.
urlstring or nullCanonical watch URL built from the ID.
statusstringOK, NO_CAPTIONS, UNAVAILABLE, BLOCKED, ERROR or INVALID_INPUT.
messagestring or nullExplanation for every status except OK.
titlestring or nullVideo title.
channelIdstring or nullChannel ID.
channelNamestring or nullChannel name.
lengthSecondsinteger or nullVideo length in seconds.
viewCountinteger or nullView count at the time of the request.
keywordsarray of stringsKeywords set by the uploader.
isLiveboolean or nulltrue for live streams and live recordings.
languagestring or nullLanguage code of the returned track, for example en or de-DE.
languageNamestring or nullDisplay name of the returned track, for example English (auto-generated).
trackKindstring or nullmanual or auto-generated.
availableLanguagesarray of objectsEvery caption track the video offers: languageCode, name, trackKind.
segmentCountinteger or nullNumber of caption segments.
wordCountinteger or nullNumber of whitespace-separated words in text. Not meaningful for languages written without spaces.
transcriptDurationSecondsnumber or nullEnd time of the last segment in seconds.
textstring or nullThe whole transcript as one string.
segmentsarray or null{ start, duration, text } per caption segment, times in seconds with millisecond precision. null when includeSegments is false.
attemptsintegerAttempts used for this video.
scrapedAtstring or nullISO 8601 timestamp (UTC) of the record.

Status values

StatusMeaningCounts as a transcript
OKA caption track was downloaded and parsed.Yes
NO_CAPTIONSThe video is playable but has no track that matches your settings. message lists the languages that exist.No
UNAVAILABLEThe video does not exist, is private, age-restricted or an unavailable live recording. message carries the reason given by YouTube.No
BLOCKEDEvery attempt was answered with a bot check. Raise maxAttempts or run again.No
ERROREvery attempt failed with a network error, an HTTP error or an empty caption body. message carries the last error.No
INVALID_INPUTThe entry is not a video URL or ID.No

Example record

This record was produced by a cloud run on 2026-10-21 (video "Me at the zoo").

{
"input": "jNQXAC9IVRw",
"videoId": "jNQXAC9IVRw",
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"status": "OK",
"message": null,
"title": "Me at the zoo",
"channelId": "UC4QobU6STFB0P71PMvOGN5A",
"channelName": "jawed",
"lengthSeconds": 19,
"viewCount": 445555035,
"keywords": ["me at the zoo", "jawed karim", "first youtube video"],
"isLive": false,
"language": "en",
"languageName": "English",
"trackKind": "manual",
"availableLanguages": [
{ "languageCode": "en", "name": "English", "trackKind": "manual" },
{ "languageCode": "de", "name": "German", "trackKind": "manual" }
],
"segmentCount": 6,
"wordCount": 39,
"transcriptDurationSeconds": 18.881,
"text": "All right, so here we are, in front of the elephants the cool thing about these guys is that they have really... really really long trunks and that's cool (baaaaaaaaaaahhh!!) and that's pretty much all there is to say",
"segments": [
{ "start": 1.2, "duration": 2.16, "text": "All right, so here we are, in front of the elephants" },
{ "start": 5.318, "duration": 2.656, "text": "the cool thing about these guys is that they have really..." },
{ "start": 7.974, "duration": 4.642, "text": "really really long trunks" },
{ "start": 12.616, "duration": 1.751, "text": "and that's cool" },
{ "start": 14.421, "duration": 1.312, "text": "(baaaaaaaaaaahhh!!)" },
{ "start": 16.881, "duration": 2, "text": "and that's pretty much all there is to say" }
],
"attempts": 1,
"scrapedAt": "2026-10-21T04:53:35.386Z"
}

Example of a record that is not OK

{
"input": "nfWlot6h_JM",
"videoId": "nfWlot6h_JM",
"status": "NO_CAPTIONS",
"message": "No caption tracks are available for this video.",
"language": null,
"text": null,
"segments": null,
"attempts": 1
}

Only the fields that matter here are shown. The real record contains all fields from the reference table, with the video details filled in.

Run summary

The key-value store record SUMMARY is written at the end of every run. This is the summary of a real run with 33 videos:

{
"startedAt": "2026-10-21T04:55:44.907Z",
"finishedAt": "2026-10-21T04:56:51.402Z",
"recordsWritten": 33,
"duplicatesSkipped": 0,
"statusCounts": { "OK": 31, "NO_CAPTIONS": 2 },
"transcriptsReturned": 31,
"attemptsTotal": 37,
"averageAttemptsPerTranscript": 1.13,
"languageCounts": { "en": 30, "ko": 1 },
"trackKindCounts": { "manual": 26, "auto-generated": 5 },
"segmentsTotal": 2642,
"wordsTotal": 19346,
"chargeLimitReached": false
}

attemptsTotal counts all attempts of the run, including those of records that are not OK. averageAttemptsPerTranscript averages the attempts of OK records only.

Pricing

The Actor has no per-result price at the moment: you pay only the Apify platform usage of your runs, listed below. The Actor is prepared for the pay-per-event model with one event, transcript-extracted, for every record with status OK. If that pricing is switched on, records with any other status stay free and the price is shown on the Actor page before you run it.

Under pay-per-event pricing, if you set a maximum charge for the run, the Actor stops starting new videos once the limit is reached and sets chargeLimitReached in the summary. Videos that were already in flight may not be written to the dataset, because the platform stops accepting data over the limit.

Platform usage per transcript

Platform usage (residential proxy traffic, compute, storage) is billed to the account that runs the Actor while the Actor has no per-result price; under pay-per-event pricing the Actor owner would cover it instead. Measured in cloud runs on 2026-10-21 with 512 MB of memory on a free-plan account, so unit prices on your plan may differ:

RunVideosOKPlatform usagePer OK transcript
Default input, three videos33USD 0.00127USD 0.00042
Mixed set with a 4.4 hour and a 3.4 hour course97USD 0.00478USD 0.00068
33 music videos and talks, concurrency 103331USD 0.01486USD 0.00048

Residential traffic was about 55 KB per transcript and 92 percent of the cost in the 33-video run. Long videos are not expensive: the whole second run, including the two multi-hour courses (about 2,900 segments each), used about 0.55 MB of residential traffic, because the caption XML is transferred compressed.

The 33-video run includes one proxy connection that stalled for about a minute, which raised its compute share. The Actor now cuts every request after 22 seconds and retries it on a new IP.

Reliability

Measured on 2026-10-21 from the Apify US residential pool, 49 valid video requests in 6 cloud runs:

  • 43 returned a transcript, 4 returned NO_CAPTIONS (2 videos without captions, 2 caused by the settings includeAutoGenerated: false and fallbackToAnyLanguage: false that were used in tests), 2 returned UNAVAILABLE (a non-existent ID and a live stream).
  • 42 of the 49 videos reached their final answer on the first attempt. The other 7 failed their first attempt and reached a final answer on the second attempt. For 5 of these the logged reason was a bot check (2), a proxy UPSTREAM504 (2) or an ECONNRESET (1); the other 2 happened before retry logging was added. No video needed a third attempt, and no video ended as BLOCKED or ERROR.
  • Without a residential proxy the same endpoint answered most requests with the bot check in earlier tests (1 of 5 videos passed from a cloud datacenter, 1 of 3 from a local connection), so the proxy is part of the design and not an option.

This is a small sample from one day. YouTube changes its anti-bot rules without notice, so check the statusCounts in the summary of your own runs.

Usage via API

Replace YOUR_USERNAME with the account that owns the Actor and YOUR_APIFY_TOKEN with an API token.

cURL

curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~youtube-transcript-extractor/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"videos": ["jNQXAC9IVRw", "https://youtu.be/dQw4w9WgXcQ"],
"languages": ["en"],
"includeSegments": false
}'

The synchronous endpoint waits for the run and has a time limit of 300 seconds. For large batches start an asynchronous run with POST /v2/acts/YOUR_USERNAME~youtube-transcript-extractor/runs and read the dataset after the run has finished.

Python

from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("YOUR_USERNAME/youtube-transcript-extractor").call(
run_input={
"videos": ["jNQXAC9IVRw", "https://youtu.be/dQw4w9WgXcQ"],
"languages": ["en"],
"includeSegments": True,
}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item["status"] == "OK":
print(item["videoId"], item["language"], item["trackKind"], item["wordCount"])
else:
print(item["input"], item["status"], item["message"])

Node.js

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('YOUR_USERNAME/youtube-transcript-extractor').call({
videos: ['jNQXAC9IVRw', 'https://youtu.be/dQw4w9WgXcQ'],
languages: ['en'],
includeSegments: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) {
if (item.status === 'OK') console.log(item.videoId, item.language, item.trackKind, item.wordCount);
else console.log(item.input, item.status, item.message);
}

Tips

  • To get subtitles for a specific language, put it first in languages and set fallbackToAnyLanguage to false. Videos without that language then come back as NO_CAPTIONS with the list of available languages in message.
  • If you only need the text, set includeSegments to false. The dataset is much smaller and the cost is the same.
  • To build a clean training or search corpus, set includeAutoGenerated to false and filter on trackKind.
  • For lists of hundreds of videos raise maxConcurrency to 10. Residential traffic per transcript does not depend on concurrency.
  • Retry only the failed videos: filter the dataset for BLOCKED and ERROR records and run the Actor again with their input values.

Limitations and Notes

  • The Actor depends on an undocumented YouTube endpoint and on the ANDROID client identity. YouTube can change or remove it without notice. The client version is fixed in src/player.js.
  • Residential proxies are required. In tests without one, most videos were answered with the bot check, while the residential pool returned a final answer for every video within two attempts. Running with useApifyProxy: false is possible but most videos come back as BLOCKED.
  • Only public videos that offer a caption track are supported. Private, age-restricted and removed videos come back as UNAVAILABLE. A 24/7 live stream was also returned as UNAVAILABLE in a test; members-only videos were not tested.
  • Translated captions (automatic translation into another language) are not supported. Only the tracks listed in availableLanguages can be returned.
  • Auto-generated captions can contain overlapping segments, because YouTube shows two lines at once. text is built from the segments in order, so overlapping lines are not merged.
  • Emoji characters are removed from titles and caption text. Text symbols such as music note signs, the copyright sign and the trademark sign are kept.
  • wordCount splits on whitespace, so it is not a reliable length measure for Chinese, Japanese and Thai.
  • viewCount and the other video details are taken from the player response at the time of the request. The player response of this client does not contain the publication date, so the Actor does not return one.
  • Playlists, channels, search results and comments are out of scope. Pass individual video URLs or IDs.
  • Run locally with apify run only for testing: accounts without the external proxy access feature cannot use Apify Proxy outside the platform, and a direct connection from a datacenter is usually blocked.
  • You are responsible for using the data in line with the YouTube Terms of Service and the copyright of the video owners. This Actor is not affiliated with or endorsed by YouTube or Google.

Local development

npm install
npm test
apify run

npm test runs the unit tests of the input parser, caption parser, player response classifier, retry logic and the consistency checks between the code defaults and the files in .actor. The tests use real responses that were captured from YouTube as fixtures, and a fake network boundary for the retry logic.