TikTok Transcript Scraper: Captions + AI Speech-to-Text
Pricing
from $2.14 / 1,000 caption transcripts
TikTok Transcript Scraper: Captions + AI Speech-to-Text
TikTok video transcripts, no login: video links or creator handles in, the spoken words out. TikTok's own captions with timed cues, SRT and WebVTT; AI speech-to-text where a video has none. Likes, comments, shares, saves, the author, the sound. Pinned videos told apart. No words, no charge.
Pricing
from $2.14 / 1,000 caption transcripts
Rating
0.0
(0)
Developer
Hyperbach
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
0
Monthly active users
3 days ago
Last modified
Categories
Share
TikTok video transcripts, no login: video links or creator handles in, the spoken words out. TikTok's own caption track first — the clean text, its timed cues, the raw WebVTT, SRT and the language on every captioned video — and AI speech-to-text where TikTok has no track (most ordinary videos), priced apart and only where it runs. Every row carries the video's metadata: the description, the posting date to the second, duration, plays, likes, comments, shares and saves as TikTok's page shows them, the author with followers, the sound. A creator's newest videos by date with the pinned ones told apart, depth past the newest ten through a browser walk, a date window, a watchlist that delivers only new videos; TikTok's machine-translated tracks per language; the timed words of a model row; the video file. A video without words — music only, a photo post — is an honest free row, and so is every miss.
Why this TikTok transcript scraper
- TikTok's own words first, at a caption price. Where TikTok carries a caption track (its automatic speech recognition, or the creator's own captions), the row gets that track: the clean text with one space between cues (no glued words), the 15 timed cues of a 36-second clip, the raw WebVTT, SRT, the language and the track's source (ASR, MT or creator). The leader of the lane charges the same price for a
transcript: nullrow on a video it cannot read; here a video without words is free, and the words are on the row, never behind a file link that expires. - Speech-to-text where there is no track — the default, not an afterthought. 77 % of ordinary TikToks carry no caption track and 81 % of those have words (measured over 130 videos of 14 ordinary creators). By default such a video's audio goes to a speech model (OpenAI's gpt-4o-mini-transcribe, the dated snapshot) and the row says so (
transcript_source= model) at its own price; TikTok's captioned videos stay at the caption price. Choosealwaysfor the model on every video, orofffor captions only. - No invented words. The model answers empty on music-only clips where other models invent text; a fragment of fewer than five words over music is treated as no speech (the fragment is kept in
notes); a word the model may have misheard for a slur is read twice before it stands. A clip without speech is a freeno_speechrow. - A creator's newest videos, by date — the pinned ones told apart. Three rivals returned a creator's pinned videos when asked for the two newest. Here the newest N count unpinned videos only,
is_pinnedis on every creator row,includePinnedadds the pinned ones marked. Up to ten come without a browser; more are read by walking the creator's list in a browser, with a COMPLETE or TRUNCATED verdict against TikTok's own count.onlyNewturns a scheduled creator task into a feed of new videos only. - The video's metadata on every row. The posting date to the second (
created_at), the description with its line breaks, duration, likes, comments, shares, saves and plays as TikTok's page shows them (statsV2: exact up to 10,000, rounded by TikTok above it, e.g. 45,200 where the app shows 45,171), the author with followers, the sound and TikTok's original-sound flag. The row says which country read it (read_country): TikTok shows a different comment count per region. - Honest and bounded. No start fee, no platform usage billed to you, no plan cap: a FREE account runs 100 videos. The log says the most a run can cost before the first request,
maxItemsis exact, the charge limit is kept before the model is called, and a run cut by its own timeout delivers what it read and names what it did not. Every miss — a removed video, a missing creator, a bad value, a refused exit — is a free row withstatusandreason.
Who it's for
- Creators, agencies and ghostwriters — the scripts of a creator's latest TikToks with the hook, the date and the counts, to study what works and write the next one.
- Researchers and analysts — what is said across many videos, with exact dates, counts and the spoken language, from video links or whole creators.
- Content and ad teams — subtitles in SRT or WebVTT straight from TikTok's own cues, the timed words of a model row, the video file, ready for an editor.
- Developers and agents — one row shape, typed fields, a free labelled row for every video without a transcript, a run summary, and a dataset-chaining input for a pipeline.
Quick start
One video, the words
{"videoUrls": ["https://www.tiktok.com/@hubermanlab/video/7578955632024309022"]}
A creator's five newest videos
{"handles": ["garyvee"],"maxVideosPerHandle": 5}
A creator's last 30 days, pinned videos included
{"handles": ["garyvee"],"maxVideosPerHandle": 50,"dateFrom": "30 days","includePinned": true}
TikTok's Spanish translation beside the words
{"videoUrls": ["https://www.tiktok.com/@hubermanlab/video/7578955632024309022"],"captionLanguages": ["es"]}
The speech model on every video, with timed words
{"videoUrls": ["https://www.tiktok.com/@hubermanlab/video/7578955632024309022"],"aiTranscription": "always","timestamps": true}
Chain another scraper's dataset
{"datasetId": "<the dataset id of a TikTok scraper run>"}
Output
One row per video: the transcript and its labels, the video's metadata, the options you asked for — or, for a video without a transcript, the same row with status and reason saying why (free):
| field | meaning |
|---|---|
status | ok = the row carries a transcript (charged: caption_transcript or ai_transcript by transcript_source). Otherwise free, with reason saying why: no_caption, no_speech, photo_post, not_found, unavailable, no_videos, invalid, audio_too_large, audio_failed, transcription_failed, refused, skipped, failed. |
reason | Why the row has no transcript, in words (null on ok rows). |
input | The value you gave that brought the video into the run (a video link, an id, a share link, a creator's handle). |
input_kind | What the input was read as: video, share, handle, invalid. A creator's listed videos carry video with the handle in input. |
input_index | The input's position in your list (1 = first); null for a video that came from a dataset. |
video_id | TikTok's video id (19 digits; stable for the video's life; its top 32 bits are its creation second). Deduplicate on this. |
video_url | The video's link (/photo/ for a photo post). |
author_username | The creator's handle. |
author_id | The creator's user id. |
author_nickname | The creator's display name. |
author_verified | True when the creator's account is verified. |
author_followers | The creator's follower count as the video page shows it (rounded by TikTok above 10,000). |
description | The video's written description (the caption text under the video, with its hashtags), as written; not the transcript. |
created_at | When the video was posted, to the second (ISO 8601, UTC). |
created_at_timestamp | The posting time as a Unix timestamp. |
duration_s | The video's length in seconds (0 for a photo post). |
plays | Plays as the TikTok web page shows them: rounded by TikTok (99,900 where the app shows 99,885). |
likes | Likes at the time of the run, as the web page shows them (exact up to 10,000, rounded by TikTok above). |
comments | Comments count as shown to the reading region (read_country): TikTok's count differs by region; rounded by TikTok above 10,000. |
shares | Shares (exact up to 10,000, rounded by TikTok above). |
saves | Saves (bookmarks; exact up to 10,000, rounded by TikTok above). |
is_pinned | On a creator run: true when the video is pinned to the top of the creator's profile (delivered only with includePinned), false for one of the newest. Null on a pasted link: TikTok's page carries no pinned flag. |
is_photo_post | True for a photo post (a slideshow of images): no spoken track, a free photo_post row. |
image_count | For a photo post: how many images it has. |
text_language | The language TikTok assigns to the video's text (ISO 639-1), as the page carries it. |
music_id | The sound's id. |
music_title | The sound's title ("original sound - |
music_author | The sound's author as TikTok names it. |
music_original | TikTok's original-sound flag: true for an "original sound" (a creator's own audio, this video's or another video's — TikTok sets it for another creator's original sound too), false for a licensed song. The speech model reads the sound's mp3 only when the sound was made with this video and lasts as long (audio_source). |
music_url | The sound's mp3 on TikTok's CDN (open; expires). |
cover_url | The video's cover image on TikTok's CDN (expires). |
play_url | The video file on TikTok's CDN (signed; served with the page session's cookie; expires at media_expires_at). |
media_expires_at | When the first of TikTok's signed media links on the row expires, decoded from the links themselves. |
transcript | The spoken words of the whole video, as one text: TikTok's caption track (its cues joined with single spaces) or the speech model's words (transcript_source). |
transcript_source | Where the words came from: caption (TikTok's own caption track) or model (the speech model). |
transcript_model | The speech model that wrote the words of a model row (gpt-4o-mini-transcribe-2025-12-15); null on a caption row. |
word_count | Words in transcript. |
language | The spoken language's code (ISO 639-1): the caption track's language on a caption row; on a model row by the timing model with timestamps, else detected from the text (see language_source). Null when there is too little text to judge. |
language_name | The spoken language in words ("English"). |
language_source | Where language came from: caption (TikTok's track code), whisper-1 (the timing model, with timestamps) or text-detection (a detector over the transcript's text). |
caption_language | TikTok's own code of the caption track the words came from (eng-US, spa-ES); null on a model row without a track. |
caption_source | The caption track's source as TikTok labels it: ASR (TikTok's speech recognition), MT (a machine translation), LC (the creator's own captions). |
caption_tracks | Every caption track TikTok lists for the video on the page as read: language (TikTok's code), language_code (ISO 639-1), source (ASR / MT / LC). Null when the video has none. |
caption_text | With aiTranscription: always on a video that has a TikTok track: the track's clean text beside the model's words. |
segments | The timed segments, start and end in seconds and text: TikTok's own cues on a caption row; the timing model's segments on a model row with timestamps. |
segment_count | How many timed segments the row has. |
words | With timestamps on a model row: every word with its start and end in seconds (OpenAI whisper-1). |
srt | The transcript as SRT subtitles, built from the segments (valid timecodes). |
vtt | The WebVTT: TikTok's caption file as served on a caption row; built from the timing model's segments on a model row with timestamps. |
speech_seconds | With timestamps on a model row: seconds of the video that carry speech (the segments' lengths summed). |
timing_model | With timestamps on a model row: the model that timed the words and segments (whisper-1). |
hook_3s | The opening hook: the cues that start inside the first 3 seconds of speech on a caption row, the words of the first 3 seconds of speech with timestamps, else the transcript's first 8 words (see hook_source). |
hook_start_s | With timestamps on a model row: the second the first word is spoken at. |
hook_source | How the hook was cut: caption_cues (TikTok's cues inside the first 3 s of speech), timed_words (the first 3 s of timed words) or first_words (the first 8 words). |
translations | With captionLanguages: TikTok's machine-translated caption tracks in the requested languages, each with language (TikTok's code), language_code, source (MT), text, segment_count and the raw vtt. A requested language TikTok has no track for is named in notes. |
translation_count | How many translated tracks the row carries (the caption_language events charged); null when none. |
audio_source | On a model row: which file carried the audio to the model: original_sound_mp3 (the video's own original sound as mp3, made with the video and as long, 17 % of the video's bytes) or video_file (the video itself: a licensed song, another creator's sound, or a creator's earlier sound re-used, whose mp3 holds another video's words). |
audio_url | The file that was transcribed, on TikTok's CDN (signed; expires). |
transcript_from | live = read or transcribed in this run; cache = a model transcript served from the shared transcript cache by the video's id (the metadata on the row is still this run's). |
transcribed_at | On a model row: when the words were transcribed (UTC): the model call's time, or the cached answer's. |
long_video_minutes | Started minutes of audio past the first on a model row (the long_video_minute events charged); null when none. |
timestamp_minutes | With timestamps on a model row: started minutes of audio (the timestamps events charged); null when none. |
walk_units | 1 on the first charged row of a creator whose videos came from a browser walk (the video_walk event charged once per creator); null otherwise. |
media_files | With downloadMedia: the files written to the run's key-value store, each with kind, key, url, bytes, content_type, source_url, reused. |
video_file_url | With downloadMedia: video: a link to the stored video file that does not expire. |
is_new | On a memory run (onlyNew or stateStoreName): true when no earlier run of this memory delivered the video. |
first_seen_at | On a memory run: the day this memory first saw the video. |
read_country | The country the video page was read from (readCountry): the comment count and the caption CDN host follow it. |
notes | Anything the row could not carry in full, in words (which egress read the caption or audio, a requested translation TikTok has no track for, a fragment the model heard on music, a region TikTok answered for). |
scraped_at | When the row was read (UTC). |
Example record:
{"status": "ok","reason": null,"input": "https://www.tiktok.com/@hubermanlab/video/7578955632024309022","input_kind": "video","input_index": 1,"video_id": "7578955632024309022","video_url": "https://www.tiktok.com/@hubermanlab/video/7578955632024309022","author_username": "hubermanlab","author_id": "6681031681057506309","author_nickname": "Andrew Huberman","author_verified": true,"author_followers": 667900,"description": "Don't Start a Conversation With an Apology @thinkfasttalksmart #huberman #hubermanlabpodcast #hubermanlab #publicspeaking #speakingtips","created_at": "2025-12-01T18:19:13Z","created_at_timestamp": 1764613153,"duration_s": 36,"plays": 99900,"likes": 4794,"comments": 27,"shares": 155,"saves": 790,"is_pinned": null,"is_photo_post": false,"image_count": null,"text_language": "en","music_id": "7578959635990858527","music_title": "original sound - Andrew Huberman","music_author": "Andrew Huberman","music_original": true,"music_url": "https://v16m.tiktokcdn-us.com/7c94ea1a0315b6e1d35f4297bb29dfe1/6ad1e130/video/tos/useast8/tos-useast8-v-27dcd7-tx2/ocB3CI6P04iPiOEtA5iiA4BSKlBEvgPMIof6qu/?a=1233&bti=ODszNWYuMDE6&&bt=125&ft=GSDrKInz7ThzpqKGXq8Zmo&mime_type=audio_mpeg&rc=OWlnODM7Zmg6aTxkaWk4aEBpamhtM3M5cjx3NzMzaTU8NEA2NC5fNC8xNS8xXjY2M2FiYSM0aDVoMmRjbGphLS1kMTJzcw%3D%3D&vvpl=1&l=20261016023211F8091DC3120DD73AE098&btag=e000c8000","cover_url": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/os0AKNtRMBLEuAiKxiwI1BkAAlBaAxfAT43BiP~tplv-tiktokx-origin.image?dr=9636&x-expires=1792288800&x-signature=Q%2BZGwmpRLfWC%2BK5Kz5IP%2BIzBHWU%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast8","play_url": "https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-ve-0068c003-tx2/oADhKE0QbE3WfFMEUqofEfQZhPDRCrA2tSI7AI/?a=1988&bti=ODszNWYuMDE6&&bt=710&ft=4KJMyMzm8Zmo0ZFjIa4jV6tZQpWrKsd.&mime_type=video_mp4&rc=OTlmaThpaDtnODc6MzM2OkBpam52bnQ5cnl3NzMzaTgzNEBiMjIyMl8yNS8xMGMvMzZjYSM1cjRhMmRzamphLS1kLzJzcw%3D%3D&expire=1792290768&l=20261016023211F8091DC3120DD73AE098&ply_type=2&policy=2&signature=47ef8078de46f5e3806d40ade6d3171f&tk=tt_chain_token&btag=e00088000","media_expires_at": "2026-10-18T02:00:00Z","transcript": "Apologizing is a way of trying to connect, yet I think it can backfire. So instead of saying, I'm sorry or I didn't do work, start with something that you're confident in. And if you do need to apologize because there's something you didn't do or you need to do, have that come later. The way you start an interaction sets the tone for the whole interaction. And if I start by putting myself one down because I apologize, oh, I do, I'm not prepared, or I should have done this differently, then our interaction is going to be very different. So start with something that you're comfortable with and confident in, and then if you need to apologize, apologize later. It really makes a big difference.","transcript_source": "caption","transcript_model": null,"word_count": 127,"language": "en","language_name": "English","language_source": "caption","caption_language": "eng-US","caption_source": "ASR","caption_tracks": [{"language": "eng-US","language_code": "en","source": "ASR"}],"caption_text": null,"segments": [{"start": 0.02,"end": 4.0,"text": "Apologizing is a way of trying to connect,"},{"start": 4.74,"end": 6.34,"text": "yet I think it can backfire."},{"start": 6.341,"end": 9.701,"text": "So instead of saying, I'm sorry or I didn't do work,"},{"start": 9.702,"end": 11.981,"text": "start with something that you're confident in."},{"start": 11.982,"end": 13.421,"text": "And if you do need to apologize"},{"start": 13.422,"end": 15.741,"text": "because there's something you didn't do or you need to do,"},{"start": 15.742,"end": 19.401,"text": "have that come later. The way you start an interaction"},{"start": 19.78,"end": 21.5,"text": "sets the tone for the whole interaction."},{"start": 21.501,"end": 24.661,"text": "And if I start by putting myself one down because I apologize,"},{"start": 24.662,"end": 25.021,"text": "oh, I do,"},{"start": 25.022,"end": 27.201,"text": "I'm not prepared, or I should have done this differently,"},{"start": 27.46,"end": 29.74,"text": "then our interaction is going to be very different."},{"start": 29.741,"end": 32.581,"text": "So start with something that you're comfortable with and confident in,"},{"start": 32.582,"end": 34.021,"text": "and then if you need to apologize,"},{"start": 34.022,"end": 36.47,"text": "apologize later. It really makes a big difference."}],"segment_count": 15,"words": null,"srt": "1\n00:00:00,020 --> 00:00:04,000\nApologizing is a way of trying to connect,\n\n2\n00:00:04,740 --> 00:00:06,340\nyet I think it can backfire.\n\n3\n00:00:06,341 --> 00:00:09,701\nSo instead of saying, I'm sorry or I didn't do work,\n\n4\n00:00:09,702 --> 00:00:11,981\nstart with something that you're confident in.\n\n5\n00:00:11,982 --> 00:00:13,421\nAnd if you do need to apologize\n\n6\n00:00:13,422 --> 00:00:15,741\nbecause there's something you didn't do or you need to do,\n\n7\n00:00:15,742 --> 00:00:19,401\nhave that come later. The way you start an interaction\n\n8\n00:00:19,780 --> 00:00:21,500\nsets the tone for the whole interaction.\n\n9\n00:00:21,501 --> 00:00:24,661\nAnd if I start by putting myself one down because I apologize,\n\n10\n00:00:24,662 --> 00:00:25,021\noh, I do,\n\n11\n00:00:25,022 --> 00:00:27,201\nI'm not prepared, or I should have done this differently,\n\n12\n00:00:27,460 --> 00:00:29,740\nthen our interaction is going to be very different.\n\n13\n00:00:29,741 --> 00:00:32,581\nSo start with something that you're comfortable with and confident in,\n\n14\n00:00:32,582 --> 00:00:34,021\nand then if you need to apologize,\n\n15\n00:00:34,022 --> 00:00:36,470\napologize later. It really makes a big difference.\n","vtt": "WEBVTT\n\n00:00:00.020 --> 00:00:04.000\nApologizing is a way of trying to connect,\n\n00:00:04.740 --> 00:00:06.340\nyet I think it can backfire.\n\n00:00:06.341 --> 00:00:09.701\nSo instead of saying, I'm sorry or I didn't do work,\n\n00:00:09.702 --> 00:00:11.981\nstart with something that you're confident in.\n\n00:00:11.982 --> 00:00:13.421\nAnd if you do need to apologize\n\n00:00:13.422 --> 00:00:15.741\nbecause there's something you didn't do or you need to do,\n\n00:00:15.742 --> 00:00:19.401\nhave that come later. The way you start an interaction\n\n00:00:19.780 --> 00:00:21.500\nsets the tone for the whole interaction.\n\n00:00:21.501 --> 00:00:24.661\nAnd if I start by putting myself one down because I apologize,\n\n00:00:24.662 --> 00:00:25.021\noh, I do,\n\n00:00:25.022 --> 00:00:27.201\nI'm not prepared, or I should have done this differently,\n\n00:00:27.460 --> 00:00:29.740\nthen our interaction is going to be very different.\n\n00:00:29.741 --> 00:00:32.581\nSo start with something that you're comfortable with and confident in,\n\n00:00:32.582 --> 00:00:34.021\nand then if you need to apologize,\n\n00:00:34.022 --> 00:00:36.470\napologize later. It really makes a big difference.\n","speech_seconds": null,"timing_model": null,"hook_3s": "Apologizing is a way of trying to connect,","hook_start_s": null,"hook_source": "caption_cues","translations": null,"translation_count": null,"audio_source": null,"audio_url": null,"transcript_from": "live","transcribed_at": null,"long_video_minutes": null,"timestamp_minutes": null,"walk_units": null,"media_files": null,"video_file_url": null,"is_new": null,"first_seen_at": null,"read_country": "US","notes": ["caption read proxy"],"scraped_at": "2026-10-16T02:32:09Z"}
Pricing
Pay only for the transcripts a run delivers, with no start fee and no platform usage billed to you. Prices fall with your Apify plan.
| per 1,000 | Free | Starter | Scale | Business |
|---|---|---|---|---|
Caption transcript (caption_transcript) | $3.00 | $2.55 | $2.14 | $2.14 |
AI transcript (ai_transcript) | $28.79 | $24.47 | $20.15 | $14.49 |
Caption translation (caption_language) | +$2.00 | +$1.70 | +$1.40 | +$1.00 |
Video file (media_file) | +$1.84 | +$1.84 | +$1.84 | +$1.84 |
| per unit | Free | Starter | Scale | Business |
|---|---|---|---|---|
Long video minute (long_video_minute) | +$0.015 | +$0.01275 | +$0.0105 | +$0.00926 |
Timestamps, per minute (timestamps) | +$0.02 | +$0.0185 | +$0.0185 | +$0.0185 |
Video walk (video_walk) | +$0.10 | +$0.10 | +$0.10 | +$0.10 |
A video with TikTok's own caption track is $3.00 per 1,000 on the Free plan ($0.003 a video) and $2.14 on Business, whatever its length. A video whose words come from the speech model is $28.79 per 1,000 on Free ($0.02879 a video) and $14.49 on Business, the first minute of audio included. A walk is $0.10 per creator handle whose list is read past the 10 newest videos (maxVideosPerHandle over 10), once per handle on every plan; a walk that gets no list from TikTok is not charged (the run says PARTIAL and the embed's 10 newest stand in). + fees are charged only on a row that carries the thing: a long-video minute per started minute of audio past the first on a model row; timestamps per started minute of audio on a model row that carries the timed words (with timestamps on); a caption translation per machine-translated track delivered (with captionLanguages); a video file per file written to the run's store this run (with downloadMedia); a video walk once per creator whose list is read past the newest ten (maxVideosPerHandle over 10, or a date window past them), charged on that creator's first charged row. A video without speech, a photo post, a video without a track when the model is off, a removed or private video, a creator with no videos, a bad value, a model failure and a video onlyNew skipped are labelled rows and free; so is a run that failed. maxItems counts transcript rows and is exact; before the first request the log says the most the run can cost at your plan's prices and names the fees of the options you switched on. Enterprise plans have their own rates — the Actor's Pricing tab shows the price for your plan.
Usage patterns
- Captions first, the model where there is none — By default (
aiTranscription: fallback) a video with a TikTok caption track gets that track (thecaption_transcriptevent) and a video without one goes to the speech model (theai_transcriptevent, priced apart).alwayssends every video to the model and keeps TikTok's track beside ascaption_text;offdelivers captions only and makes a video without one a freeno_captionrow. In every mode a video the model hears no speech in is a freeno_speechrow, and a photo post a freephoto_postrow. - A creator feed — Save a task with a creator,
onlyNewandmaxVideosPerHandle, and schedule it daily: each run lists the creator's newest videos and delivers and charges only those it has not delivered before — a day without a new video delivers nothing. A video without words is remembered too (its verdict is final); a video that could not be read is tried again on the next run. Apify's integrations send the rows to a sheet, a webhook or Slack. - Depth: more than the ten newest — Up to ten videos per creator come from TikTok's embed with no browser.
maxVideosPerHandleabove ten walks the creator's video list in a browser (onevideo_walkevent, $0.10 per creator handle, the run at 2,048 MB; a walk that gets no list from TikTok is not charged and the embed's 10 newest stand in) and reads the captions from the list itself; the run summary says how many videos the walk listed against TikTok's own count (COMPLETE or TRUNCATED). A date window that reaches past the ten newest needsmaxVideosPerHandleabove ten to walk; at ten or less the row says so. - Subtitles, translations and files — A caption row carries TikTok's own timed cues as
segments, the raw WebVTT asvttand an SRT built from the cues;captionLanguagesadds TikTok's machine-translated track per language intranslations(text and WebVTT) where TikTok has one;timestamps: trueadds the timed words and the timing model's segments on a model row;downloadMedia: "video"writes the video file to the run's key-value store with a link that does not expire (video_file_url). TikTok's own media links on the row (play_url,music_url,cover_url) expire within hours:media_expires_atsays when. - From another scraper — Give
datasetIdthe dataset of a TikTok scraper run (the clockworks scrapers, scrape-creators, dz_omar and the like): every row is scanned for a video link (webVideoUrl,url,videoUrl) or a videoid, and the videos are transcribed with their metadata read live. With Apify's 'Connect actor' integration the id is filled automatically. - Many videos — Paste any number of video links, ids or creators;
maxItemsbounds the charged rows across every input. The run reads videos on several exits at once and transcribes them in parallel; the log line at the start says the most the run can cost at your plan's prices.
Input configuration
| field | type | default | what it does |
|---|---|---|---|
videoUrls | array | [] | TikTok videos, one per line, in any form: https://www.tiktok.com/@hubermanlab/video/7578955632024309022, a /photo/ link, a bare 19-digit video id (the video_id column of an earlier run), a share or short link (vm.tiktok.com/…, tiktok.com/t/…: followed to the video it lands on). A creator's handle pasted here (garyvee, @garyvee) is read as a creator. The same video given several ways is one row. |
handles | array | [] | TikTok creators, one per line: garyvee, @garyvee, https://www.tiktok.com/@garyvee or a share link to a profile. The Actor lists each creator's newest videos by posting date (a video pinned to the top of the profile is told apart and left out unless includePinned is on) and transcribes up to maxVideosPerHandle of them. A missing or private creator is a free row. |
datasetId | string | "" | The dataset id of another TikTok scraper's run (the clockworks scrapers, scrape-creators, dz_omar and the like): every row is scanned for a video link (webVideoUrl, url, videoUrl) or a video id and transcribed. Empty = none. |
maxVideosPerHandle | integer | 10 | How many of each creator's newest videos to transcribe, newest by posting date, pinned videos not counted. Up to 10 come from TikTok's embed; more are read by walking the creator's video list in a browser, with a COMPLETE or TRUNCATED verdict against TikTok's own count in the run summary. A video without words counts (it is a free row); a video outside the date window does not. |
includePinned | boolean | false | Creators only: also deliver the videos pinned to the top of the profile (up to 3), flagged is_pinned, besides the newest maxVideosPerHandle. Off = the newest videos by date only; a pinned video is never taken for one of the newest. |
dateFrom | string | "" | Creators only: videos posted on or after this date (2026-09-01), or a span back from today (30 days, 2 weeks, 6 months). The embed serves a creator's 10 newest: a window that reaches past them needs maxVideosPerHandle above 10 (the browser walk, which stops at the first video older than this). |
dateTo | string | "" | Creators only: videos posted on or before this date (same forms as dateFrom). |
aiTranscription | fallback / always / off | "fallback" | Where the words come from when TikTok has no caption track for a video (77 % of ordinary clips): fallback sends the video's audio to a speech model and charges the ai_transcript event; always sends every video to the model (the most consistent words across a creator's videos; TikTok's own track, when there is one, rides along as caption_text); off delivers TikTok's captions only and makes a video without one a free no_caption row. A video the model hears no speech in is a free no_speech row in every mode. |
captionLanguages | array | [] | ISO 639-1 codes (es, fr, pt…), up to 5: TikTok's machine-translated caption track in each language, with its text and WebVTT in translations (one more page read per language, charged per track delivered). TikTok offers translations for some languages per video, not all: a language it has no track for is said in notes, free. |
timestamps | boolean | false | On a row the speech model transcribed: add the timed words (words), the model's timed segments, srt and vtt, the speech seconds and the hook's start second, and the spoken language as named by the timing model (OpenAI whisper-1). Charged per started minute of audio on top of the ai_transcript event. A caption row carries TikTok's own timed cues, SRT and WebVTT in every mode. |
downloadMedia | none / video | "none" | Store the video file in the run's key-value store, with a link that does not expire on the row (video_file_url). Charged per file written. TikTok's own media links on the row expire within hours (media_expires_at). |
readCountry | US / GB / DE / FR | "US" | The country the video page is read from. TikTok shows the comment count of the reader's region (32 from the US and 35 from Germany on one video in the same minute); every row says which country read it (read_country). Canada and Brazil are not offered: TikTok refuses visitors from them on the first call. |
maxItems | integer | 0 | The most transcripts this run delivers (and charges), captions and model rows together. 0 = no limit. Free rows (no speech, photo posts, not found) do not count. |
onlyNew | boolean | false | Skip videos this task delivered in an earlier run: run a creator daily and get only the new videos. A video without words is remembered too (its verdict is final); a video that could not be read is tried again next run. Turns the memory on. |
stateStoreName | string | "" | Name of the memory between runs (onlyNew, is_new, first_seen_at). Empty = derived from your videos and creators, so the same task always meets its own memory. |
proxyUrl | string | Optional. Your own proxy as scheme://user:pass@host:port, used instead of the Actor's own residential exits for TikTok's pages (Apify's residential proxy stays the fallback when yours fails). Leave empty to use the maintained exits (recommended). |
Errors
A run does not fail because of your input. When an input cannot be used — a date that is not a date, an id in the wrong form, two settings that contradict each other — the run ends Succeeded, charges nothing, and says what to change:
- the status message starts with
INPUT REJECTED; - the dataset holds one row,
{"error": true, "code": "…", "message": "…"}, and no results; - the key-value store record
ERRORholds the same object.
From code, check error on the first row before reading results.
A search that matches nothing is not an error: the dataset is empty and nothing is charged. A run that ends Failed is a fault on our side, never your input; it charges nothing, and we are alerted.
FAQ
Is it TikTok's caption or the spoken words?
Both, labelled. Where TikTok has a caption track the row carries it (transcript_source = caption, caption_source = ASR for TikTok's speech recognition, MT for a machine translation, LC for the creator's own captions). Where TikTok has none the speech model's words are the transcript (transcript_source = model, transcript_model names it). The video's written description is on the row too, as description, clearly apart.
What about a music-only clip, a photo post or a video without a track?
A free row with the video's metadata: no_speech when the speech model hears no words (music-only, silent, or a fragment too short to be speech), photo_post for a slideshow of images (no spoken track; with aiTranscription: always a slideshow with the creator's own sound goes to the model), no_caption for a video without a TikTok track when the speech model is off.
Which counts are exact?
Every count is what TikTok's web page shows (statsV2): exact up to 10,000 and rounded by TikTok above it — likes 45,200 where the app shows 45,171, plays 99,900 where it shows 99,885, followers 668,000. The comment count also depends on the reader's region (32 from the US and 35 from Germany on one video in the same minute): readCountry picks it and read_country says it on every row. No logged-out page serves the app's unrounded figures.
Why are pinned videos not among the newest?
A creator pins up to three videos to the top of the profile, and TikTok lists them first whatever their date (one of garyvee's is from 2021). Asked for the two newest, three rivals returned the pinned ones. Here the newest N count unpinned videos only; includePinned adds the pinned ones, flagged is_pinned, in their date position.
Which languages do the translations come in?
TikTok's own machine-translated caption tracks, where TikTok offers them for the video: on our test video Spanish and French were offered, German, Portuguese, Italian and Japanese were not. A requested language TikTok has no track for is named in notes and costs nothing. The transcript itself is always the track in the video's own language.
Why no comment texts, hashtag or keyword lists, or a Canadian or Brazilian view?
The comment texts sit behind a signed route TikTok serves to its own app only; keyword, hashtag and sound lists are outside this Actor's scope (it reads videos and creators); TikTok refuses visitors from Canada and Brazil on the first call. This Actor reads what a visitor from the US, GB, Germany or France sees, with no account and no cookie beyond the page's own, so it does not promise them.
What is never charged?
A video without speech, a photo post, a video without a TikTok track when the speech model is off, a removed or withheld video, a creator with no videos, a bad value, a share link that lands nowhere, an audio the CDN refused, a model failure, a video skipped by onlyNew, a video the time limit cut before its transcription, the metadata, and a run that failed before its first row. A run that stops early (a timeout, an abort) is charged for the rows it delivered.
Are the timestamps exact?
A caption row carries TikTok's own cue times (segments, srt, vtt): the cues of a 36-second clip start at 0.02 s and end at 36.47 s. A model row carries timed words and segments only with timestamps: true (OpenAI whisper-1 gives each word's start and end in seconds); without it the model row has the words alone.
How long can a video be?
Any length TikTok allows: the whole audio goes to the model in one call (its upload limit is 25 MB; the original-sound mp3 of a 10-minute video is about 10 MB). The first minute is in the AI transcript price; each started minute past the first is a long_video_minute event. A caption row costs the same whatever the length.
What does the transcript cache do?
A video the speech model transcribed once is kept on our side by its own video id and model: a later run of the same video, by anyone, gets the same words with no new transcription (transcript_from = cache, transcribed_at = when it was read). The metadata on the row is always this run's live read. TikTok's own captions are read live every run (the page carries the fresh track).
Can an AI agent use it (MCP)?
Yes. Apify's MCP server (https://mcp.apify.com) offers Store Actors as tools to Claude, Cursor and other MCP clients: add this Actor to the server's tool list and the agent sends videoUrls or handles and gets the rows back. The synchronous API (run-sync-get-dataset-items) does the same in one HTTP call.
Integration
JavaScript
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_TOKEN' });const run = await client.actor('hyperbach/tiktok-transcript-scraper').call({"videoUrls": ["https://www.tiktok.com/@hubermanlab/video/7578955632024309022"]});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Python
from apify_client import ApifyClientclient = ApifyClient('YOUR_TOKEN')run = client.actor('hyperbach/tiktok-transcript-scraper').call(run_input={'videoUrls': ['https://www.tiktok.com/@hubermanlab/video/7578955632024309022']})items = client.dataset(run['defaultDatasetId']).list_items().items
CLI
$apify call hyperbach/tiktok-transcript-scraper --input '{"videoUrls": ["https://www.tiktok.com/@hubermanlab/video/7578955632024309022"]}'
REST
curl -X POST "https://api.apify.com/v2/acts/hyperbach~tiktok-transcript-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \-H 'Content-Type: application/json' -d '{"videoUrls": ["https://www.tiktok.com/@hubermanlab/video/7578955632024309022"]}'
Support
This page is generated from the Actor's schemas and a live sample — it cannot describe a field the Actor does not have.