# TikTok Transcript Scraper - $0.40 per 1,000 (`dami_studio/tiktok-transcript-scraper`) Actor

No caption track, no charge - a TikTok without one has nothing to transcribe. The rest come back as full transcript, timed segments, SRT and WebVTT, detected language, plus video id, author, caption and view count. $0.40 per 1,000 transcripts.

- **URL**: https://apify.com/dami\_studio/tiktok-transcript-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media, Videos, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.40 / 1,000 transcript scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## TikTok Transcript Scraper

Paste TikTok video links, bare video ids or `vm.tiktok.com` share links, and get the spoken words back as text. Every row carries the full transcript, the timed segments behind it, the detected language, and the video it came from: id, author, caption, duration, upload date and view count.

No account to set up, no cookies to paste, no browser, no API key.

- It reads the caption track TikTok itself generates for the video, so the words match what the app shows on screen. Nothing is re-transcribed and nothing is guessed.
- Every row ships four shapes of the same transcript: flat `text`, timed `segments`, ready-made `srt`, and the original `vtt`.
- A video with no speech is never charged. It comes back as a free diagnostic row naming the video, so you can see what was skipped.
- Ask for a specific language and you get the translated caption track when TikTok has one, with `isTranslated` marking those rows.
- Up to 1,000 links per run, fetched in parallel, deduplicated before anything is billed.
- Empty input returns one labelled sample row, free.

### Price

**$0.40 per 1,000 transcripts.** Plus **$0.0005 per run** as the platform start fee, billed per gigabyte of run memory, so exactly that on the default 1 GB.

No volume tiers, no minimum spend, no subscription.

| Transcripts | Total |
|---|---|
| 100 | $0.0405 |
| 1,000 | $0.4005 |
| 10,000 | $4.0005 |
| 100,000 | $40.0005 |

#### What gets charged

- One `transcript-scraped` event per transcript row written to the dataset. Nothing else is metered per row.
- Free: the sample row an empty run returns, and every diagnostic row. They all carry `"charged": false`.
- A video with no caption track is a free `NO_TRANSCRIPT` row, not a charge.
- Deleted, private, age-restricted and region-locked videos are free diagnostic rows too.
- A link pasted twice, or pasted once as a share link and once as a full URL, is fetched once and charged once.
- A run that finds nothing costs the start fee and nothing else.
- Rows never leave the dataset without a charge and are never charged without a row. The billed event is a named one, so nothing is quietly attached to `apify-default-dataset-item`.

### Input

```json
{
  "videoUrls": [
    "https://www.tiktok.com/@baobei55555j/video/7516208601434819862",
    "https://www.tiktok.com/@studywithlizzz/video/7657803613355412766",
    "7673152431932378389"
  ],
  "maxItems": 10,
  "concurrency": 6
}
```

| Field | What it does |
|---|---|
| `videoUrls` | The videos you want transcribed. A full link, a `vm.tiktok.com` or `vt.tiktok.com` share link, or the bare numeric video id. All three work and you can mix them. Up to 1,000 per run. |
| `maxItems` | How many transcripts to return at most. The list is read in order and the run stops as soon as it has this many, so it's a spend cap as much as a row cap. Links past that point cost nothing. Default 10, hard ceiling 5,000. |
| `language` | A language code such as `es`, `fr`, `de` or `ja`. When TikTok holds a translated caption track for the video you get that one and the row is marked `isTranslated: true`. When it doesn't, you get the original spoken track instead of an error. Leave empty for the original. |
| `concurrency` | How many videos to read at once. Default 6, maximum 12. Lower it on a very long list if you want to be gentle. There's no reason to raise it for small runs. |
| `sessionCookies` | Optional, and empty is the normal case. See [Age-restricted videos](#age-restricted-videos). |
| `proxyUrls` | Leave empty. Only for callers who want traffic to leave through proxy servers they already pay for, as `http://user:pass@host:port`. |

Run it with empty input and you get one labelled sample row, free.

### Output

One row per transcript. This is a real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "transcript",
  "videoId": "7516208601434819862",
  "url": "https://www.tiktok.com/@baobei55555j/video/7516208601434819862",
  "inputUrl": "https://www.tiktok.com/@baobei55555j/video/7516208601434819862",
  "authorUsername": "baobei55555j",
  "authorName": "Baobei5",
  "authorUrl": "https://www.tiktok.com/@baobei55555j",
  "caption": "Real NASA Footage? Scientist Walks Moon! #NASA #MoonMission #MoonLanding #Space #Astronaut",
  "hashtags": ["NASA", "MoonMission", "MoonLanding", "Space", "Astronaut"],
  "durationSeconds": 8,
  "createdAt": "2025-06-15T16:08:11.000Z",
  "language": "eng-US",
  "languageCode": "en",
  "languageName": "English",
  "isAutoGenerated": true,
  "isTranslated": false,
  "captionSource": "ASR",
  "text": "So, guys, we've just landed on the moon. And as you can see, Tori, this view is insane. It's freezing out here. This is just day 1. More to come.",
  "wordCount": 30,
  "characterCount": 145,
  "segmentCount": 5,
  "segments": [
    {
      "start": 0,
      "end": 0.46,
      "startTime": "00:00:00.000",
      "endTime": "00:00:00.460",
      "text": "So, guys,"
    },
    {
      "start": 0.461,
      "end": 1.741,
      "startTime": "00:00:00.461",
      "endTime": "00:00:01.741",
      "text": "we've just landed on the moon."
    }
  ],
  "srt": "1\n00:00:00,000 --> 00:00:00,460\nSo, guys,\n\n2\n00:00:00,461 --> 00:00:01,741\nwe've just landed on the moon.",
  "vtt": "WEBVTT\n\n00:00:00.000 --> 00:00:00.460\nSo, guys,\n",
  "availableLanguages": ["eng-US"],
  "playCount": 40900,
  "likeCount": 795,
  "commentCount": 24,
  "shareCount": 18,
  "musicTitle": "original sound - aiunderstandyou4",
  "coverUrl": "https://p16-common-sign.tiktokcdn.com/tos-no1a-p-0037-no/o8aRuyBFEiAfIwCTGAikA7CAF2u4fVUOog4Bgt~tplv-tiktokx-origin.image",
  "scrapedAt": "2026-08-16T15:44:23.728Z"
}
```

#### Field notes

- `text` is the whole transcript as one readable string, with caption line breaks collapsed and stray spaces before punctuation cleaned up. The words themselves are untouched.
- `segments` is the same transcript as timed cues: `start` and `end` in seconds, `startTime` and `endTime` as `HH:MM:SS.mmm`, and the line of text. Use this for clipping, search-inside-video, or lining a transcript up with the footage.
- `srt` is the cues rebuilt as a standard SRT file, ready to save as `.srt`.
- `vtt` is the original WebVTT exactly as served, for anyone who wants the untouched source.
- `isAutoGenerated` is true when the track came from speech recognition rather than being written by a person.
- `isTranslated` is true when the track is a machine translation rather than the language actually spoken.
- `language` is the track's own code, like `eng-US`. `languageCode` is the two-letter form and `languageName` the English name, so you don't have to map it yourself.
- `availableLanguages` lists every caption track TikTok exposed for that video on that request, so you can see what else could have been asked for.
- `durationSeconds` is the video length, not the transcript length. A long video with two spoken words has a long duration and a short transcript.
- `playCount` is views at read time. It keeps moving.
- `inputUrl` is exactly what you pasted, so rows join back to your own list. `url` is the canonical link with the real author handle filled in.

Real rows carry `"charged": true`. Sample rows carry `"_sample": true`, diagnostic rows carry `"_diagnostic": true` and an `errorCode`. Neither is billed.

### How it works

It asks TikTok for the video's own page the way a phone browser does and reads the caption track out of the data the page already carries. One request per video, no page rendering, no headless browser, no login.

The caption file itself is fetched from TikTok's subtitle store and parsed into timed cues, then rebuilt into plain text and SRT. Requests leave through a rotating pool of addresses, so a per-address rate limit is answered by moving rather than waiting. Videos are read in parallel, with a small ladder of retries per video so one bad moment on one address doesn't cost you a row. Two independent readers run against every page, so a layout change on TikTok's side doesn't take the Actor down with it.

### Age-restricted videos

Every video is read without an account. That's the fast path and it's what nearly all of them answer.

A few won't. An age-restricted video is the clear case: TikTok shows it only to a signed-in viewer. Those come back as free `NOT_FOUND` diagnostic rows and the run carries on with the rest of your list.

If you need them, paste your own TikTok cookie into `sessionCookies`:

1. Sign in to TikTok in Chrome.
2. `F12` → Application → Cookies → `https://www.tiktok.com`.
3. Copy the `sessionid` value and pass it as `"sessionid=<value>"`.

The cookie goes to TikTok's video pages and nowhere else. Caption files come from a CDN and no cookie is sent with them. Its value is never written to the log. You can pass several and the run rotates between them. The run's status line reports `sessionState` so you can tell which kind of run you had: `keyless` when nothing needed an account, then `ok`, `degraded` or `exhausted`.

### How much of TikTok actually has a transcript

TikTok creates a caption track when it detects speech, and creators can upload their own. It doesn't create one for a clip that's only music, only sound effects, or silent, and a photo slideshow has no audio track at all.

On a random 120-video sample pulled from TikTok's own topic pages, 85 videos had a caption track and 35 did not. Talking-head, tutorial, news and review content is close to universally covered. Dance, comedy skits without dialogue, and music edits usually aren't.

The 35 that had nothing cost nothing. Each came back as a free `NO_TRANSCRIPT` row naming the video. So the worst case for a list of links that turns out to be all music is a run that bills the start fee and nothing else.

### What people use it for

- Feeding TikTok content to a language model for summaries, topic tagging or translation, without paying a speech-to-text bill for audio that already has captions.
- Repurposing short video into blog posts and newsletters. The transcript is the first draft.
- Content research at scale. Pull the transcripts of a hundred videos in a niche and search them for the phrases and objections that keep coming up.
- Subtitle work. Take the `srt` or `vtt` straight into an editor, retime it, translate it.
- Accessibility archives, where the spoken content of published videos has to be kept in text form.
- Building a searchable index of a creator's back catalogue, so "which video mentioned X?" becomes a text query.
- Hook analysis. `segments` gives you the first three seconds of speech separately from the rest, which is the part that decides whether anyone watches.

### Reading the output

- **Real rows** carry `"charged": true` and `"recordType": "transcript"`. One billed event each.
- **The sample row** carries `"_sample": true` and `"charged": false`. There's exactly one, only when the input had no videos.
- **Diagnostic rows** carry `"_diagnostic": true`, `"charged": false` and an `errorCode`: `NO_TRANSCRIPT` when the video has no caption track, `NOT_FOUND` when it's deleted, private, age-restricted or region-locked, `BAD_INPUT` when a line wasn't a TikTok link, `BLOCKED` when the target refused the request, `NETWORK` when it couldn't be reached, `TIME_BUDGET` when time ran out. Each carries a plain-English `error` and the `inputUrl` it belongs to.

Filter on `charged == true` and you have exactly the rows you paid for.

### Limits

- A video only has a transcript if TikTok generated a caption track for it or the creator uploaded one. Roughly seven in ten videos in a mixed sample have one. Silent clips, music-only edits and photo slideshows don't, and those come back as free diagnostic rows.
- Nothing here listens to audio. If TikTok has no caption track, this Actor won't invent one. It tells you the track is missing instead.
- Auto-generated captions are speech recognition, so they carry its mistakes. Proper nouns, brand names, numbers and heavy accents are where they slip. Punctuation and sentence boundaries are approximate.
- Translated tracks (`isTranslated: true`) are machine translations, not human subtitles.
- Which translated languages exist is TikTok's decision per video, not a setting. Asking for a language TikTok doesn't have for that video returns the original spoken track, marked as such, rather than failing.
- On-screen text burned into the picture (stickers, overlays, meme captions) isn't part of the caption track and isn't returned.
- Private, friends-only, deleted, age-restricted and region-locked videos come back as free diagnostic rows saying which it was.
- Timings come from the caption file, so they're cue timings rather than word-level timings. There's no per-word timestamp.
- View, like, comment and share counts are a snapshot at read time.
- Hard ceilings: 1,000 links and 5,000 transcripts per run.
- Live videos and stories are out of scope. This reads regular posted videos.
- The run works down your list until it has the number of transcripts you asked for, so extra links are a useful buffer against videos that turn out to have none. It gives up early only when a list is almost entirely caption-less, roughly three fruitless lookups per transcript requested and never fewer than 25, and it says so in a free diagnostic row rather than failing.

### Questions

**What happens if a video has no captions?**

One free diagnostic row with `errorCode: "NO_TRANSCRIPT"` naming the video, and the run carries on.

**Can I paste share links from the app?**

Yes. `vm.tiktok.com`, `vt.tiktok.com` and `tiktok.com/t/` links are all followed to the real video. You can also paste the bare numeric video id, and mix all of these in one list.

**Does it transcribe the audio?**

No, deliberately. It reads the caption track TikTok already made, which is the same text the app shows on screen. That's faster, it costs a fraction of running speech-to-text, and for the videos that have it, it's the better transcript.

**Can I get the transcript in another language?**

Set `language` to a code like `es` or `fr`. When TikTok holds a translated caption track for that video you get it, marked `isTranslated: true`. When it doesn't, you get the original spoken track rather than an error, so a batch never dies because one video wasn't translated.

**Do I need a proxy?**

No. The run brings its own egress and its cost is already inside the price. `proxyUrls` exists only for callers who want traffic to leave through servers they already own.

**Will the run fail if something goes wrong?**

No. A blocked, deleted or caption-less video produces an uncharged diagnostic row and the run still finishes as succeeded. A failed run would still bill the start fee, which would mean paying to be told something went wrong.

**Can I run it on a schedule?**

Yes. Nothing is held between runs. Use `videoId` to work out which rows are new since last time.

# Actor input Schema

## `videoUrls` (type: `array`):

The videos you want transcribed. A full link (https://www.tiktok.com/@user/video/7516208601434819862), a vm.tiktok.com or vt.tiktok.com share link, or just the bare numeric video id - all three work, and you can mix them in one list. Up to 1,000 per run.

## `maxItems` (type: `integer`):

How many transcripts to return at most. The list is read in order and the run stops as soon as it has this many, so links past that point cost you nothing. Keep it low while you are testing - you pay per transcript.

## `language` (type: `string`):

A language code such as es, fr, de or ja. When TikTok holds a translated caption track for the video you get that one and the row is marked isTranslated: true. When it does not, you get the original spoken track instead of an error. Leave empty for the original.

## `concurrency` (type: `integer`):

How many videos to read in parallel. Default 6, maximum 12. Lower it if you are working through a very long list and want to be gentle with the target.

## `sessionCookies` (type: `array`):

Optional, and empty is the normal case. Almost every video is read without any account at all. A few - age-restricted ones especially - are only shown to a signed-in visitor, and without a cookie those come back as free NOT\_FOUND diagnostic rows. Paste your own TikTok cookie here to read them, on a rate limit nobody else shares. In Chrome: F12 -> Application -> Cookies -> tiktok.com, copy the sessionid value and pass it as sessionid=<value>. It goes to TikTok video pages only, is never written to the log, and is never saved to the dataset.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.tiktok.com/@baobei55555j/video/7516208601434819862",
    "https://www.tiktok.com/@studywithlizzz/video/7657803613355412766"
  ],
  "maxItems": 10,
  "language": "es"
}
```

# Actor output Schema

## `results` (type: `string`):

Every row in the default dataset: videoId, authorUsername, caption, durationSeconds, language, isAutoGenerated, isTranslated, segmentCount, wordCount, text, url, createdAt, playCount, inputUrl, authorName, authorUrl, hashtags, languageCode, languageName, captionSource, characterCount, segments, srt, vtt, availableLanguages, likeCount, commentCount, shareCount, musicTitle, coverUrl, scrapedAt. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "videoUrls": [
        "https://www.tiktok.com/@baobei55555j/video/7516208601434819862",
        "https://www.tiktok.com/@studywithlizzz/video/7657803613355412766"
    ],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/tiktok-transcript-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "videoUrls": [
        "https://www.tiktok.com/@baobei55555j/video/7516208601434819862",
        "https://www.tiktok.com/@studywithlizzz/video/7657803613355412766",
    ],
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/tiktok-transcript-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "videoUrls": [
    "https://www.tiktok.com/@baobei55555j/video/7516208601434819862",
    "https://www.tiktok.com/@studywithlizzz/video/7657803613355412766"
  ],
  "maxItems": 10
}' |
apify call dami_studio/tiktok-transcript-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/tiktok-transcript-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ivAZhrFSILw5KPsxb/builds/ZOrnyaVf0eQQtGdHW/openapi.json
