TikTok Transcript Scraper
Pricing
$1.00 / 1,000 video transcripts
TikTok Transcript Scraper
Get transcripts of public TikTok videos from links or a creator's newest videos. Output plain text, timed segments, SRT or VTT, with language choice and video stats. No login. Videos that can't be read get an error code and aren't charged. You pay per video with a transcript.
Pricing
$1.00 / 1,000 video transcripts
Rating
0.0
(0)
Developer
Michael Lip
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
an hour ago
Last modified
Categories
Security
Limited permissionsShare
TikTok Transcripts and Captions
Get the transcript of any public TikTok video as plain text, timed segments, SRT or WebVTT for $0.001 per video, and pay nothing for videos that fail.
Paste video links, short links, bare IDs or @creator profiles. The Actor reads the captions TikTok already has for each video (its own speech recognition, or captions the creator added) and saves them with the video's stats. It doesn't log in, doesn't run a browser and doesn't download the video. Leave the sample link in the form and click Start to see one real row.
What you get
One dataset row per video. The video details come from the same page, so they cost nothing extra. Turn them off with "Include video details".
| Field | Type | What it holds |
|---|---|---|
input | string | The line you typed (or a backup sample link, see the FAQ) |
url, videoId | string | Canonical video link and ID |
profile | string or null | The username of the profile the video came from, when you gave a profile |
status | ok or error | Whether a transcript was saved |
errorCode, errorMessage | string or null | Why a row failed. See the error codes below |
language | string | Language of the first transcript, like en |
text | string | The transcript of the first language (the one in language) as one string (with the text format). The other languages are in transcripts |
availableLanguages | array | Every caption language TikTok lists for the video |
transcripts | array | One entry per language, see the next table |
description | string | The post caption |
createdAt | ISO date | Upload time, UTC |
durationSeconds | number | Video length |
authorUsername, authorName, authorVerified, authorFollowers | mixed | The creator |
playCount, likeCount, commentCount, shareCount, saveCount | number | Stats at scrape time |
hashtags | array | Hashtags on the post |
musicTitle | string | Sound title |
textLanguage, locationCreated, isAd | mixed | As TikTok reports them |
scrapedAt | ISO date | When the row was saved |
Each entry in transcripts holds one language.
| Field | What it holds |
|---|---|
language | Short code, like en |
tiktokLanguage | TikTok's own label, like eng-US |
source, autoGenerated | TikTok's source label for the track, passed through as TikTok sends it. ASR is TikTok's speech recognition, and autoGenerated is true for it. The Actor doesn't interpret other labels, like MT in the example below. For those, autoGenerated is true or false when TikTok's caption data says so, and null when it doesn't |
captionFormat | Format of the file TikTok served |
segmentCount, endSeconds | Number of caption lines and the end time of the last one |
text | The transcript for this language (with the text format) |
segments | [{ start, end, text }] in seconds (with the segments format) |
srt, vtt | Subtitle files as strings (with the srt or vtt format) |
Example output
Here's the row from a run on Apify's servers on 2026-11-05, using the prefilled input with no proxy. We cut the two text values and description, and kept 2 of the 18 segments. Every other value is as saved.
{"input": "https://www.tiktok.com/@tedtoks/video/7704416343474441502","url": "https://www.tiktok.com/@tedtoks/video/7704416343474441502","videoId": "7704416343474441502","profile": null,"status": "ok","errorCode": null,"errorMessage": null,"language": "en","text": "We need to talk about democracy better. We're gonna ask people to put democracy over party. ...","transcripts": [{"language": "en","tiktokLanguage": "eng-US","source": "MT","autoGenerated": false,"captionFormat": "webvtt","segmentCount": 18,"endSeconds": 59.976,"text": "We need to talk about democracy better. We're gonna ask people to put democracy over party. ...","segments": [{ "start": 0.12, "end": 2.2, "text": "We need to talk about democracy better." },{ "start": 2.201, "end": 5.1, "text": "We're gonna ask people to put democracy over party." }]}],"availableLanguages": ["en"],"description": "If we can tell a better story about democracy, then we can tell a better story about America, says political strategist Sarah Longwell. ...","createdAt": "2026-11-04T20:30:39.000Z","durationSeconds": 60,"authorUsername": "tedtoks","authorName": "TED Talks","authorVerified": true,"authorFollowers": 3300000,"playCount": 4356,"likeCount": 287,"commentCount": 5,"shareCount": 3,"saveCount": 35,"hashtags": [],"musicTitle": "original sound - TED Talks","textLanguage": "en","locationCreated": "US","isAd": false,"scrapedAt": "2026-11-05T15:14:32.100Z"}
The run also writes a summary to the OUTPUT record in the key-value store, with counts per error code and the number of duplicate lines it skipped.
Price
It's pay per event. You pay for videos that come back with a transcript, and that's the only thing the Actor charges.
| Event | Price | When it's charged |
|---|---|---|
| Video transcript | $0.001 | Once per video saved with at least one transcript, whatever the number of languages or formats |
| Error rows | $0 | No captions, not found, private, blocked, skipped and every other error code |
Some worked examples.
| You send | Transcripts saved | Actor cost |
|---|---|---|
| The prefilled sample | 1 | $0.001 |
| 100 video links | 100 | $0.10 |
| 1,000 video links, 200 of them without speech, max videos set to 1,000 | 800 | $0.80 |
| 1,000 video links, 3 languages each, max videos set to 1,000 | 1,000 | $1.00 |
| 20 profiles, 10 newest videos each, max videos set to 200 | up to 200 | up to $0.20 |
With the default "Max videos per run" of 100, videos past the first 100 get free SKIPPED rows, so raise it for bigger lists. You can also set a maximum cost per run in Apify, and the run stops cleanly when it gets there. The videos left over get free SKIPPED rows too.
Input
| Field | What it does | Default |
|---|---|---|
| Video URLs | Video links, short links (vm.tiktok.com, vt.tiktok.com, tiktok.com/t/), embed links or bare video IDs. Up to 1,000. | One sample video. It's left out when you add profiles and it's the only video line |
| Profile URLs | @user or https://www.tiktok.com/@user. Up to 50. | Empty |
| Newest videos per profile | 1 to 10 | 5 |
| Languages | Codes like en, es, pt-BR or eng-US, up to 10. Empty means the video's spoken language. all means every track. Cantonese is yue, and zh means Mandarin. | Empty |
| Output formats | text, segments, srt, vtt | text and segments |
| Include video details | Adds the stats and author fields | On |
| Max videos per run | 1 to 1,000 | 100 |
| Proxy | Turn on Apify Proxy if you see BLOCKED rows. Datacenter groups and your own proxy URLs work. The residential group isn't supported, and a run that asks for it stops right away. | Off |
{"videoUrls": ["https://www.tiktok.com/@nbcnews/video/7704498999432056078","7704416343474441502"],"profileUrls": ["@tedtoks"],"videosPerProfile": 3,"languages": ["en"],"formats": ["text", "srt"]}
Use it from the API
The synchronous endpoint runs the Actor and returns the dataset rows in one HTTP call. Get your token from Apify Console under Settings, API and Integrations.
curl -s -X POST \"https://api.apify.com/v2/acts/zovo-one~tiktok-transcripts/run-sync-get-dataset-items" \-H "Authorization: Bearer $APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"videoUrls": ["https://www.tiktok.com/@tedtoks/video/7704416343474441502"], "formats": ["text"]}'
The synchronous call waits at most 300 seconds for the run. If the run takes longer, the call ends without the rows, but the run keeps going. For long lists or many profiles, start a normal run and read the dataset when it ends.
Use it from Python
$pip install apify-client
from apify_client import ApifyClientclient = ApifyClient("YOUR_APIFY_TOKEN")run = client.actor("zovo-one/tiktok-transcripts").call(run_input={"videoUrls": ["https://www.tiktok.com/@nbcnews/video/7704498999432056078"],"profileUrls": ["@tedtoks"],"videosPerProfile": 5,"formats": ["text", "srt"],})for row in client.dataset(run["defaultDatasetId"]).iterate_items():if row["status"] == "ok":print(row["url"], row["text"][:80])else:print(row["input"], row["errorCode"])
Use it from Node.js
$npm install apify-client
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('zovo-one/tiktok-transcripts').call({videoUrls: ['https://www.tiktok.com/@tedtoks/video/7704416343474441502'],languages: ['all'],formats: ['segments', 'vtt'],});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const row of items) console.log(row.status, row.url, row.errorCode ?? row.language);
Use it from AI agents (MCP)
The Apify MCP server at mcp.apify.com turns this Actor into a tool for AI agents. Each MCP client sets up a remote server in its own way. The configurator at https://mcp.apify.com/ gives the setup for your client. Clients that read an mcpServers config with a url key, such as Cursor, take the snippet below. VS Code uses a servers key with "type": "http" in .vscode/mcp.json, and Claude Desktop adds remote servers as connectors in its settings. The tools parameter in the URL loads this Actor as a tool. The Actor runs with limited permissions, which the MCP server needs before it will call an Actor.
{"mcpServers": {"apify": {"url": "https://mcp.apify.com?tools=zovo-one/tiktok-transcripts"}}}
With the URL alone, your browser opens on first connect so you can sign in with OAuth. To use a token instead, add a header.
{"mcpServers": {"apify": {"url": "https://mcp.apify.com?tools=zovo-one/tiktok-transcripts","headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }}}}
Then ask in plain words, for example "Get the transcript of https://www.tiktok.com/@tedtoks/video/7704416343474441502 and summarize it." The agent reads the input schema, fills in the links and gets the rows back. Each row has a status and an errorCode, so the agent can tell a video without speech from a blocked request.
Error codes
Every input line gets a row, even when it fails, and error rows are free. Duplicates are the one exception. The same video ID or profile twice, or a short link to a video you also pasted, gets no row of its own, and OUTPUT counts those lines in duplicateLinesSkipped. A profile whose newest videos are all in the run already adds no new rows either.
| Code | Meaning |
|---|---|
NO_CAPTIONS | TikTok has no captions for the video, or the caption file it lists is empty. Videos without speech (music, silent comedy) have none. |
LANGUAGE_NOT_AVAILABLE | None of your languages exist for this video. availableLanguages lists the ones that do. If TikTok has one of your languages only on a host this Actor doesn't fetch from, the message says so. |
VIDEO_NOT_FOUND | Deleted or wrong ID. |
VIDEO_PRIVATE | The video or the account is private. |
VIDEO_UNAVAILABLE | TikTok returned another status code (shown in the message). |
UNSUPPORTED_POST | A photo post. These have no spoken audio. |
INVALID_URL | The line isn't a TikTok video link or ID. |
SHORT_LINK_FAILED | The short link didn't lead to a video. |
CAPTION_DOWNLOAD_FAILED | TikTok listed captions but the file couldn't be read. You also get it when TikTok's caption server keeps answering 403 or 429, and when the caption file for your language sits on a host this Actor doesn't fetch from (only TikTok's own hosts are allowed). In that last case the message names the host. |
BLOCKED | TikTok kept answering 403 or 429 after retries. |
FETCH_FAILED | Network errors or server errors after retries. |
PAGE_CHANGED | TikTok's page didn't have the video data where we expect it. You also get this when TikTok shows a login, region or check page instead of the video, or a page for a different video than the ID you asked for. Treat it like BLOCKED. |
PROFILE_NOT_FOUND, PROFILE_PRIVATE, PROFILE_NO_VIDEOS | The profile line couldn't give any videos. A profile line also gets PAGE_CHANGED when TikTok returns another account's page, so that account's videos aren't charged under your line. |
PROFILE_RATE_LIMITED | TikTok kept rate limiting the profile page. |
SKIPPED | Over "Max videos per run", over your max cost per run, or the run hit its timeout. Profiles also get it after 3 profiles in a row were rate limited, because the rest would most likely fail the same way. |
If a video has some of your languages but not all, the row is still ok and errorMessage starts with partial.
A block shows up as a failed run, not a quiet empty one. The run fails when no transcript was saved and every row that made a request was blocked, failed on the network, got PAGE_CHANGED or had its captions on a host this Actor doesn't fetch from. Lines that aren't TikTok links and photo posts don't count, since they make no request.
Limits
- Profiles come from TikTok's public embed page, which lists about 10 of a creator's newest videos. So you get at most 10 per profile. For older videos, paste their links.
- The transcript is TikTok's own caption track. If TikTok's speech recognition misheard a word, you'll see the same mistake. This Actor doesn't transcribe audio itself.
- TikTok's language labels aren't always right. In our tests a French video's track was labeled
por-PT. We pass the label through as TikTok sends it. - Caption links on TikTok expire after a while, so the Actor downloads the captions during the run and puts the text in the row. Nothing in the output points at an expiring caption file.
- Up to 1,000 video links, 50 profiles and 10 languages per run.
The vtt output escapes <, > and & in the cue text (as <, > and &), so a WebVTT player shows them as written. The text, segments and srt fields keep them as plain characters.
If Apify restarts a run (a migration, or a run you resurrect), rows that are already saved aren't fetched or charged again. Lines with a SKIPPED row are tried again, so such a line can end up with an older SKIPPED row and a newer row. Keep the newer one by scrapedAt. OUTPUT counts these old rows in earlierSkippedRows.
FAQ
Does it transcribe videos that have no captions?
No. It reads the caption tracks TikTok already has. A video without any gets a free NO_CAPTIONS row. In our check of 10 public talking videos, 8 had captions.
Do I need a TikTok account, cookies or a proxy?
No account and no cookies. The prefilled run worked on Apify's servers without a proxy on 2026-11-05. If TikTok starts blocking you, you'll see BLOCKED or PAGE_CHANGED rows. Turn on Apify Proxy with a datacenter group and run again.
Can I get every video from a creator?
Only the newest ones, up to 10 per profile, because that's what TikTok's public embed page lists. For older videos, paste their links in "Video URLs".
Why did my one-click run show a different video?
A run of just the sample video, with the default language, is the input Apify uses to test the Actor every day. If the sample video has gone (deleted, private or without captions), the run tries up to 2 backup sample videos from news accounts. Each one gets its own row with the backup's link as input, and you pay only for one that gives a transcript. Backups count against "Max videos per run", so with a max of 1 there are none. If you set languages, the sample run is treated like any other run and tries no backups.
Is there a YouTube version?
Yes. TubeText YouTube Transcript Scraper does the same for YouTube videos.
Is this allowed?
The Actor only reads public videos and public profile pages, without logging in, and returns post data anyone can see on TikTok. Check that your use fits TikTok's terms and the privacy laws that apply to you.
Support Zovo
Zovo builds and maintains this Actor. If it saves you time, you can support the work on GitHub Sponsors. Sponsorship is optional, and the tool works the same without it.
Zovo is made by theluckystrike on GitHub.
More tools by Zovo
- YouTube Transcript Scraper. Gets YouTube transcripts and captions from videos, playlists and channels.
- App Store and Google Play Reviews. Gets App Store and Google Play reviews in one run, with one schema.
- Website Screenshot API. Saves full-page screenshots and PDFs of public web pages.
- Google Trends Scraper. Gets Google Trends interest over time, by region and related queries, with no browser.