TikTok Transcript Scraper avatar

TikTok Transcript Scraper

Pricing

$1.00 / 1,000 video transcripts

Go to Apify Store
TikTok Transcript Scraper

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

Michael Lip

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

an hour ago

Last modified

Security

Limited permissions

Share

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".

FieldTypeWhat it holds
inputstringThe line you typed (or a backup sample link, see the FAQ)
url, videoIdstringCanonical video link and ID
profilestring or nullThe username of the profile the video came from, when you gave a profile
statusok or errorWhether a transcript was saved
errorCode, errorMessagestring or nullWhy a row failed. See the error codes below
languagestringLanguage of the first transcript, like en
textstringThe transcript of the first language (the one in language) as one string (with the text format). The other languages are in transcripts
availableLanguagesarrayEvery caption language TikTok lists for the video
transcriptsarrayOne entry per language, see the next table
descriptionstringThe post caption
createdAtISO dateUpload time, UTC
durationSecondsnumberVideo length
authorUsername, authorName, authorVerified, authorFollowersmixedThe creator
playCount, likeCount, commentCount, shareCount, saveCountnumberStats at scrape time
hashtagsarrayHashtags on the post
musicTitlestringSound title
textLanguage, locationCreated, isAdmixedAs TikTok reports them
scrapedAtISO dateWhen the row was saved

Each entry in transcripts holds one language.

FieldWhat it holds
languageShort code, like en
tiktokLanguageTikTok's own label, like eng-US
source, autoGeneratedTikTok'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
captionFormatFormat of the file TikTok served
segmentCount, endSecondsNumber of caption lines and the end time of the last one
textThe transcript for this language (with the text format)
segments[{ start, end, text }] in seconds (with the segments format)
srt, vttSubtitle 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.

EventPriceWhen it's charged
Video transcript$0.001Once per video saved with at least one transcript, whatever the number of languages or formats
Error rows$0No captions, not found, private, blocked, skipped and every other error code

Some worked examples.

You sendTranscripts savedActor cost
The prefilled sample1$0.001
100 video links100$0.10
1,000 video links, 200 of them without speech, max videos set to 1,000800$0.80
1,000 video links, 3 languages each, max videos set to 1,0001,000$1.00
20 profiles, 10 newest videos each, max videos set to 200up to 200up 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

FieldWhat it doesDefault
Video URLsVideo 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 profile1 to 105
LanguagesCodes 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 formatstext, segments, srt, vtttext and segments
Include video detailsAdds the stats and author fieldsOn
Max videos per run1 to 1,000100
ProxyTurn 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 ApifyClient
client = 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.

CodeMeaning
NO_CAPTIONSTikTok has no captions for the video, or the caption file it lists is empty. Videos without speech (music, silent comedy) have none.
LANGUAGE_NOT_AVAILABLENone 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_FOUNDDeleted or wrong ID.
VIDEO_PRIVATEThe video or the account is private.
VIDEO_UNAVAILABLETikTok returned another status code (shown in the message).
UNSUPPORTED_POSTA photo post. These have no spoken audio.
INVALID_URLThe line isn't a TikTok video link or ID.
SHORT_LINK_FAILEDThe short link didn't lead to a video.
CAPTION_DOWNLOAD_FAILEDTikTok 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.
BLOCKEDTikTok kept answering 403 or 429 after retries.
FETCH_FAILEDNetwork errors or server errors after retries.
PAGE_CHANGEDTikTok'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_VIDEOSThe 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_LIMITEDTikTok kept rate limiting the profile page.
SKIPPEDOver "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 &lt;, &gt; and &amp;), 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