# TikTok Transcript Scraper - Video to Text, SRT & Bulk (`sauliusautomatesit/tiktok-transcript-scraper`) Actor

Transcribe public TikTok videos to text, timestamped segments, SRT and VTT. Uses TikTok's own captions when they exist and falls back to Whisper speech-to-text when they don't, so caption-less videos still return a transcript. Bulk-friendly, no API key, no cookies.

- **URL**: https://apify.com/sauliusautomatesit/tiktok-transcript-scraper.md
- **Developed by:** [Saulius Saulenas](https://apify.com/sauliusautomatesit) (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 $1.70 / 1,000 transcript from native captions

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 — TikTok video to text, SRT & bulk

**Turn public TikTok videos into transcripts, timestamps, SRT and VTT — including the ones TikTok
never captioned.**

Paste TikTok links, get the words back. Every video returns plain text, timestamped segments, a
ready-to-use `.srt` subtitle file, a `.vtt` file, and the source metadata: creator, caption,
hashtags, duration, language, view count.

Most TikTok transcript tools only read TikTok's own caption track, so they hand back nothing for
the majority of TikToks — the ones where the creator turned captions off, or TikTok never generated
them. **This Actor falls back to Whisper speech-to-text and transcribes the audio instead.**

- **No API key, no login, no cookies.** Public videos only.
- **90+ languages.** The transcript comes back in the video's own language by default.
- **Bulk-safe.** One dead link never breaks the rest of the run.
- **You only pay for delivered transcripts.** Failures, private videos and over-length videos are free.

***

### What it handles

| Input | Supported |
|---|---|
| `tiktok.com/@user/video/1234567890123456789` | ✅ |
| `vm.tiktok.com/…` and `tiktok.com/t/…` share links | ✅ (followed to the real video) |
| `tiktok.com/embed/v2/…` embed URLs | ✅ |
| `m.tiktok.com/…` mobile links | ✅ |
| Photo-mode posts | ✅ when they carry audio |
| Profile, hashtag or search URLs | ❌ — these are not single videos and return `INVALID_URL` |
| Private, friends-only, deleted or region-locked videos | ❌ — returned as an error row, uncharged |

Other platforms return a clear error pointing you at
[Short Video Transcriber](https://apify.com/sauliusautomatesit/short-video-transcriber), which
handles TikTok, YouTube Shorts and Instagram Reels in one run.

***

### Captions vs Whisper — the part that matters

TikTok publishes caption tracks for only some videos, and it says why when it does not:
the creator disabled them, TikTok's own speech recognition never ran, or it ran and found no
speech in a supported language.

1. **Native captions first.** When TikTok has a usable track, the words come from it. Fast, and
   priced at **$0.002**.
2. **Whisper when there are none.** The audio is downloaded and transcribed locally with
   faster-whisper. Priced at **$0.040**, because it costs roughly a second of CPU per 1.5 seconds
   of audio.

Where TikTok offers several tracks — its speech recognition of the original audio plus machine
translations of that into other languages — the **original language** is chosen, because a
translation of what was said is not a transcript of it. Ask for a specific `language` and you get
that track instead, translation or not.

Exactly one of those two events is charged per video, after the row is written. Never both, never
before. A video that fails is not charged at all.

***

### What you get

One dataset row per input URL:

```json
{
  "url": "https://www.tiktok.com/@shanehryhorec/video/7674894968963534100",
  "platform": "tiktok",
  "videoId": "7674894968963534100",
  "author": "shanehryhorec",
  "authorName": "Shane Hryhorec",
  "title": "Kicked off a Virgin Australia plane today. ✈️♿️",
  "hashtags": ["accessibility", "wheelchair", "travel"],
  "durationSeconds": 179.0,
  "language": "en",
  "source": "native_captions",
  "sourceDetail": "tiktok_auto_captions",
  "isAutoGenerated": true,
  "text": "I was kicked off a Virgin Australia flight today…",
  "segments": [
    { "start": 0.08, "end": 2.14, "text": "I was kicked off a Virgin Australia" },
    { "start": 2.14, "end": 4.46, "text": "flight today, and here is what happened" }
  ],
  "srt": "1\n00:00:00,080 --> 00:00:02,140\nI was kicked off a Virgin Australia\n\n2\n…",
  "vtt": "WEBVTT\n\n00:00:00.080 --> 00:00:02.140\nI was kicked off a Virgin Australia\n\n…",
  "wordCount": 695,
  "segmentCount": 118,
  "viewCount": 1840231,
  "publishedAt": "2026-08-14T10:03:11+00:00",
  "status": "ok",
  "error": null
}
```

`source` is always exactly one of:

- `native_captions` — the words came from TikTok's own caption track.
- `whisper_asr` — the video had no usable captions, so the audio was transcribed with Whisper.

The `srt` and `vtt` fields are complete, valid subtitle files. Overlapping cues from
auto-generated tracks are trimmed, so subtitles do not double up on screen.

***

### Pricing

| Event | Price | When it fires |
|---|---|---|
| `caption_transcript` | **$0.002** | A transcript came from TikTok's native captions. |
| `asr_transcript` | **$0.040** | A transcript came from Whisper because there were no captions. |
| `apify-actor-start` | **$0.00005** | Apify's standard start event, charged once per gigabyte of the run's memory. This Actor runs at 2 GB, so **$0.0001 per run**. |

Those are the Free-plan prices. Apify's paid plans get the standard Store discount off every
event — Bronze 5 %, Silver 10 %, Gold and above 15 % — so `asr_transcript` costs $0.038, $0.036 or
$0.034 on those plans and `caption_transcript` costs $0.0019, $0.0018 or $0.0017.

#### What a run actually costs

Caption availability is what moves the bill, and on TikTok it is lower than on YouTube: dance,
music, meme and lifestyle content is mostly caption-less, while talking-head, storytime,
educational and news content is largely captioned.

| 1,000 TikToks | Cost |
|---|---|
| All have captions | **$2.00** |
| 50 % captions / 50 % Whisper | **$21.00** |
| 25 % captions / 75 % Whisper | **$30.50** |
| None have captions | **$40.00** |

(Free-plan prices; a Gold plan pays 15 % less than each of these.)

Turn **"Transcribe with Whisper when a TikTok has no captions"** off to cap the price at $0.002 per
video: caption-less videos then return a `CAPTIONS_UNAVAILABLE` error, free of charge, and you
decide what to do with them.

**Nothing else is charged.** In particular you are not charged for:

- videos that are deleted, private, friends-only or region-locked;
- videos longer than your `maxDurationSeconds` cap;
- caption-less videos longer than `maxAsrDurationSeconds` (they return `ASR_DURATION_EXCEEDED`);
- videos where Whisper finds no speech at all (music-only clips);
- profile URLs, malformed URLs or links from other platforms;
- any run that fails.

***

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `videoUrls` | array | — | **Required.** TikTok video URLs, one per line. Share links work. |
| `language` | string | *(blank)* | Two-letter code (`en`, `es`, `pt`, `ru`…). Blank keeps the video's own language — usually what you want. Setting it selects that language's caption track when TikTok offers one (which may be a machine translation) and tells Whisper what to expect. |
| `allowWhisperFallback` | boolean | `true` | Off = captions only, and caption-less videos cost nothing. |
| `maxDurationSeconds` | integer | `600` | Videos longer than this are skipped, free. Max `1200`. |
| `maxAsrDurationSeconds` | integer | `180` | Length limit for the Whisper path only. Caption-less videos longer than this return `ASR_DURATION_EXCEEDED`, free. Captioned videos are unaffected. |
| `maxVideos` | integer | `1000` | Safety cap on the number of URLs processed. |
| `forceWhisper` | boolean | `false` | **Advanced.** Transcribe with Whisper even when TikTok has captions. Charged at the `asr_transcript` rate — 20× the caption price. |
| `whisperModel` | string | `base` | `tiny`, `base` or `small`. Only affects the Whisper fallback. |
| `beamSize` | integer | `1` | Whisper decoding beam width. |
| `concurrency` | integer | `5` | Videos resolved in parallel. Whisper always runs one at a time. |
| `includeFailedItems` | boolean | `true` | Keep a row (with an `error` code) for every failure, so inputs reconcile to outputs. |
| `useResidentialFallback` | boolean | `true` | Retries a request through a residential IP when TikTok answers the normal proxy with a bot check. Rarely used — TikTok serves datacenter IPs fine. |
| `proxyConfiguration` | object | Apify Proxy | Recommended. The run pins one proxy session, which is what TikTok's media CDN requires. |

#### Minimal input

```json
{
  "videoUrls": [
    "https://www.tiktok.com/@shanehryhorec/video/7674894968963534100",
    "https://vm.tiktok.com/ZMabcdefg/"
  ]
}
```

***

### Run it

#### In the Apify Console

Open the Actor, paste your links into **TikTok video URLs**, one per line, and click **Start**.
Results appear in the dataset and export as JSON, CSV, XLSX or Excel — or download the `srt`
column straight into subtitle files.

#### API

```bash
curl -X POST "https://api.apify.com/v2/acts/sauliusautomatesit~tiktok-transcript-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "videoUrls": [
          "https://www.tiktok.com/@shanehryhorec/video/7674894968963534100"
        ],
        "allowWhisperFallback": true
      }'
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")

run = client.actor("sauliusautomatesit/tiktok-transcript-scraper").call(input={
    "videoUrls": [
        "https://www.tiktok.com/@shanehryhorec/video/7674894968963534100",
        "https://www.tiktok.com/@pabloescabor.08/video/7675011438385057040",
    ],
    "allowWhisperFallback": True,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["status"] != "ok":
        print(f"{item['inputUrl']}: {item['error']} — {item['errorMessage']}")
        continue
    print(item["author"], item["language"], item["wordCount"], "words", f"({item['source']})")
    with open(f"{item['videoId']}.srt", "w", encoding="utf-8") as handle:
        handle.write(item["srt"])
```

#### JavaScript

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_TOKEN' });

const run = await client.actor('sauliusautomatesit/tiktok-transcript-scraper').call({
    videoUrls: [
        'https://www.tiktok.com/@shanehryhorec/video/7674894968963534100',
    ],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();

for (const item of items) {
    if (item.status !== 'ok') {
        console.warn(`${item.inputUrl}: ${item.error}`);
        continue;
    }
    console.log(`@${item.author} (${item.source}): ${item.text}`);
    // await fs.writeFile(`${item.videoId}.srt`, item.srt);
}
```

#### n8n, Make and Zapier

Works with the standard **Apify → Run Actor** node in n8n and the **Apify → Run an Actor** module
in Make. Point the node at this Actor, pass `videoUrls` as the input JSON, then read the dataset
items in the next step — `text` for the transcript, `srt` for a subtitle file, `error` to branch on
failures.

A common shape: a sheet or webhook supplies TikTok links → this Actor returns transcripts → an LLM
node summarises, tags or repurposes them → the result is written back. Because failures come back
as rows with an `error` field rather than as a broken run, a filter on `status == "ok"` is all the
error handling most workflows need.

***

### Errors

Failed videos get a dataset row with `status: "error"`, an `error` code and a plain-English
`errorMessage`. They are never charged.

| Code | Meaning |
|---|---|
| `VIDEO_UNAVAILABLE` | Deleted, never existed, or region-locked. |
| `VIDEO_PRIVATE` | Private, friends-only or followers-only. |
| `DURATION_EXCEEDED` | Longer than `maxDurationSeconds`. |
| `ASR_DURATION_EXCEEDED` | No captions, and longer than `maxAsrDurationSeconds`. Raise that limit to transcribe it anyway. |
| `CAPTIONS_UNAVAILABLE` | No native captions and the Whisper fallback is turned off. |
| `NO_SPEECH_DETECTED` | Whisper found no speech — a music-only or silent video. |
| `INVALID_URL` | Not a link to a single TikTok video — a profile, hashtag or search URL, for example. |
| `PLATFORM_NOT_ENABLED` | A YouTube or other non-TikTok URL. Use Short Video Transcriber for those. |
| `MEDIA_UNAVAILABLE` | The audio could not be downloaded for transcription. |
| `BLOCKED` / `RATE_LIMITED` | TikTok refused the request. Try again, ideally with a proxy. |
| `MEMORY_LIMIT` | The video does not fit in the run's memory for the chosen model. Raise the memory or pick a smaller model. |

***

### Limits and good to know

- **Public videos only.** No login, no cookies, no private or followers-only content, and no way
  to add any — that is a deliberate design choice, not a missing feature.
- **Not every video can be transcribed.** A deleted, private or region-locked video fails, and a
  video that is pure music with no speech legitimately returns `NO_SPEECH_DETECTED`. You are not
  charged for either.
- **Memory.** Run with at least 2 GB. That covers the whole duration range with the `base` model;
  the Actor refuses an impossible combination up front instead of being killed mid-run.
- **TikTok's auto-captions are auto-captions.** When a transcript comes from TikTok's own ASR
  track (`isAutoGenerated: true`), it carries that track's mistakes, and on music-heavy videos it
  can be very sparse. For maximum accuracy on a specific video, switch `forceWhisper` on — and
  accept the `asr_transcript` price — or raise `whisperModel` to `small`.
- **Caption availability changes.** TikTok backfills captions hours after upload, so the same
  video can take the Whisper path today and the caption path tomorrow.
- **Whisper is length-limited; captions are not.** A caption-less video is transcribed up to
  `maxAsrDurationSeconds` (180 s by default, which covers the overwhelming majority of TikToks).
  Past that, Whisper's cost outgrows a per-video price, so the video returns
  `ASR_DURATION_EXCEEDED` and costs you nothing. Raise the limit if you want those anyway. Videos
  **with** captions run to the full `maxDurationSeconds` at the $0.002 rate, whatever their length.
- **One URL per video.** This Actor does not crawl profiles or hashtags to find videos for you.

### What this Actor does not do

It transcribes videos. It does not scrape comments, followers or profile analytics, does not
monitor accounts, and does not summarise, translate or score sentiment — pipe the transcript into
an LLM step for that.

***

### Related searches

TikTok transcript · TikTok to text · transcribe TikTok · TikTok subtitles · TikTok SRT ·
TikTok caption extractor · TikTok video to text · download TikTok subtitles · TikTok speech to
text · bulk TikTok transcripts · TikTok VTT · TikTok transcript API

# Actor input Schema

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

TikTok video URLs to transcribe — one per line, from a single link to thousands. Share links (`vm.tiktok.com/…`, `tiktok.com/t/…`) and embed URLs work too. Profile, hashtag and search URLs are not video URLs and will be reported as errors.

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

Two-letter language code (`en`, `es`, `pt`, `de`, `ru`…). Leave blank to keep the video's own language — the default, and what you want for a transcript. TikTok often publishes machine translations alongside the original captions; setting this picks that language's track when it exists (which may be one of those translations) and tells Whisper what to expect.

## `allowWhisperFallback` (type: `boolean`):

On (default): TikToks without native captions are transcribed with Whisper speech-to-text, charged at the higher `asr_transcript` rate. Off: those videos return a `CAPTIONS_UNAVAILABLE` error and cost you nothing — the cheapest way to run large batches. Most TikToks have no usable native captions, so leaving this on is what makes the Actor useful.

## `forceWhisper` (type: `boolean`):

Off (default): TikTok's own caption track is used whenever there is one. On: every video is transcribed with Whisper and charged at the `asr_transcript` rate — 20× the caption price. Worth it only when you have judged TikTok's auto-captions too poor for your use.

## `maxDurationSeconds` (type: `integer`):

Videos longer than this are skipped with a `DURATION_EXCEEDED` error and are not charged. The default of 600 s (10 min) covers TikTok's longest uploads.

## `maxAsrDurationSeconds` (type: `integer`):

Applies only to videos with no native captions. Whisper costs roughly a second of CPU per 1.5 seconds of audio, so a per-video price only works up to a point; past this length a caption-less video returns `ASR_DURATION_EXCEEDED` and costs nothing. Videos **with** captions are never subject to this — they run to the full `maxDurationSeconds`.

## `maxVideos` (type: `integer`):

Safety cap on how many URLs from the list are processed.

## `whisperModel` (type: `string`):

Accuracy vs speed for the Whisper fallback only — videos with native captions ignore this. `base` is the recommended balance.

## `beamSize` (type: `integer`):

Decoding beam width for the Whisper fallback. 1 is fastest; higher is marginally more accurate and slower.

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

How many videos to resolve at once. Whisper transcription is always serialised regardless of this setting, to keep the run inside its memory limit.

## `includeFailedItems` (type: `boolean`):

On (default): unavailable, private, too-long and non-TikTok URLs get a dataset row carrying an `error` code, so you can reconcile inputs to outputs. Failed rows are never charged.

## `useResidentialFallback` (type: `boolean`):

On (default): when TikTok answers the normal proxy with a bot check, that one request is retried through Apify's residential proxy. TikTok serves datacenter IPs perfectly well — both its pages and its media CDN — so this route is almost never used and TikTok runs measure zero residential traffic. It exists for the occasional individually-blocked IP.

## `proxyConfiguration` (type: `object`):

Proxy used to reach TikTok. Apify Proxy is recommended: the run pins one proxy session so that a video's media file is fetched from the same IP that resolved the page, which is what TikTok's CDN requires.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.tiktok.com/@shanehryhorec/video/7674894968963534100",
    "https://www.tiktok.com/@pabloescabor.08/video/7675011438385057040"
  ],
  "allowWhisperFallback": true,
  "forceWhisper": false,
  "maxDurationSeconds": 600,
  "maxAsrDurationSeconds": 180,
  "maxVideos": 1000,
  "whisperModel": "base",
  "beamSize": 1,
  "concurrency": 5,
  "includeFailedItems": true,
  "useResidentialFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `transcripts` (type: `string`):

Every transcribed TikTok, including rows for the ones that failed.

# 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/@shanehryhorec/video/7674894968963534100",
        "https://www.tiktok.com/@pabloescabor.08/video/7675011438385057040"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sauliusautomatesit/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/@shanehryhorec/video/7674894968963534100",
        "https://www.tiktok.com/@pabloescabor.08/video/7675011438385057040",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("sauliusautomatesit/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/@shanehryhorec/video/7674894968963534100",
    "https://www.tiktok.com/@pabloescabor.08/video/7675011438385057040"
  ]
}' |
apify call sauliusautomatesit/tiktok-transcript-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sauliusautomatesit/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/Rq6FM1NreHup7pyd9/builds/psou2bHhN06LMUZpg/openapi.json
