# X (Twitter) Transcript Scraper: Subtitles, SRT and Metadata (`fetchrail/x-twitter-transcript`) Actor

Scrape X (Twitter) video transcripts and subtitles with timestamps as JSON, text, SRT or WebVTT, from captions X generated or the poster uploaded. Post metadata free on request. Pay only for videos that return a transcript.

- **URL**: https://apify.com/fetchrail/x-twitter-transcript.md
- **Developed by:** [Fetchrail](https://apify.com/fetchrail) (community)
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 transcripts

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## 📝 X (Twitter) Transcript Scraper: Subtitles, SRT and Metadata

Turn the videos in X posts into clean, timestamped text. Paste a list of post
links, choose a language and a format, and get one result per video, ready for an
LLM, a spreadsheet or a subtitle editor.

| At a glance | |
| --- | --- |
| **Price** | $1.00 per 1,000 transcripts, platform usage included |
| **Charged when** | A video returns a transcript. Everything else is free. |
| **Formats** | Timestamped segments (JSON), plain text, SRT, WebVTT |
| **Captions** | X's automatic captions and captions the poster uploaded, each labelled |
| **Links** | `x.com` and `twitter.com` posts, bare post ids, `/video/2` for one video of several |
| **Metadata** | Post text, account, duration, post date, likes, replies, hashtags: free on request |
| **AI agents** | Callable as a tool from Claude, Cursor and VS Code over MCP |

### 🧾 What's in each result

- **The transcript** in the format you chose. As segments, each line carries
  `text`, `start` and `duration` in seconds.
- **Which track you got**, in `language`, and whether X generated it or the
  poster uploaded it, in `isAsr`.
- **What you asked for**, in `preferred`, so a fallback is easy to spot.
- **Every caption track** the video has, in `captionTracks`.
- **The post's metadata**, when you turn it on: post text, account, video
  duration, post date, like and reply counts, hashtags and preview images.
- **A reason code** for every post that has no transcript, instead of a crashed
  run.

### 📌 Which X posts have a transcript

This Actor returns the captions X already shows in its player; it does not run
speech-to-text of its own. X generates captions for many videos with speech, and
posters can upload their own. That leaves gaps:

- **Not every video has captions.** In our own test of public videos in October
  2026, about half did. Speech-led clips such as interviews, briefings and
  announcements mostly had them; music-led trailers and clips of a few seconds
  mostly did not. A video without captions returns `NO_CAPTIONS`, free of charge.
- **Live broadcast replays and Spaces** have no transcripts here.
- **A post that links to a YouTube video** returns that video's YouTube
  transcript, with `site` set to `youtube`, charged as one transcript.
- **A post with several videos** needs one link per video: add `/video/1`,
  `/video/2` and so on to the post URL.
- **A post with no video**, or one that only links to another website, returns
  an error code instead, free of charge.

### 🧠 Who it's for

| You are building | Set this |
| --- | --- |
| Media monitoring of officials, brands or newsrooms | `includeMetadata: true`, then export to CSV or Excel |
| A summarizer, classifier or RAG index | `format: json`, so every chunk keeps its timestamp |
| A prompt from a batch of posts | `format: text`, one string per video |
| Subtitles for re-editing a clip | `format: srt` or `vtt` |
| Quotes for an article, with the exact wording | Default settings: uploaded captions come first |
| An agent that answers questions about posts | The MCP setup below |

### ▶️ Run it in three steps

1. Paste X post links into **X post URLs**.
2. Optionally set a **Preferred language**, such as `en`, `de` or `pt-BR`.
3. Press **Start**, then download the results as JSON, CSV or Excel.

### ⚙️ Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `urls` | list of strings, required | | X post links or bare post ids. A link repeated in the list is dropped, so it is never charged twice in one run. |
| `language` | string | English where there is one | Preferred language code. A preference, not a filter: see below. |
| `preferAutomaticCaptions` | boolean | `false` | Within a language, return X's automatic track before the poster's own. |
| `format` | `json`, `text`, `srt`, `vtt` | `json` | Segments with timestamps, or the transcript as one string. |
| `includeMetadata` | boolean | `false` | Add the post's metadata to each result, at no extra charge. |

#### How the language is chosen

A video always returns a transcript when it has any caption track at all. Tracks
are tried in this order:

1. Your preferred language
2. English
3. The first track available

Within each language, a track the poster uploaded comes before X's automatic one,
unless `preferAutomaticCaptions` is on. Most X videos have a single track, in the
language spoken. `preferred` echoes what you asked for and `language` says what
you got, so filtering out an unwanted fallback is one comparison.

#### Recipes

**A batch of links and bare post ids**

```json
{
  "urls": [
    "https://x.com/NASA/status/1491475671058681863",
    "2098842302278254990",
    "https://twitter.com/SOME_ACCOUNT/status/POST_ID/video/2"
  ]
}
```

A bare id is the number at the end of a post link. `/video/2` picks the second
video of a post with several.

**SRT subtitles, X's automatic captions first**

```json
{
  "urls": ["https://x.com/NASA/status/1491475671058681863"],
  "preferAutomaticCaptions": true,
  "format": "srt"
}
```

This video has only the captions NASA uploaded, so that track is returned anyway,
and `isAsr` says so.

**Plain text plus post details, for a monitoring sheet**

```json
{
  "urls": ["https://x.com/NASA/status/1491475671058681863"],
  "format": "text",
  "includeMetadata": true
}
```

### 🗂️ Output

One item per URL. Here is a transcript with metadata on, shortened. With metadata
off, the fields from `description` to `extra` are left out:

```json
{
  "inputUrl": "https://x.com/NASA/status/1491475671058681863",
  "site": "x",
  "id": "1491441845964001288",
  "url": "https://x.com/NASA/status/1491475671058681863",
  "title": "NASA - 🎵 We’ve got it.  On its way to touch the Sun, our Parker Solar Probe ...",
  "preferred": { "language": null, "asr": false },
  "language": "en",
  "isAsr": false,
  "captionTracks": [
    { "language": "en", "asr": false, "name": null }
  ],
  "format": "json",
  "segmentCount": 32,
  "transcript": [
    { "text": "NASA has captured the first visible light images of the surface of Venus from space.", "start": 0.53, "duration": 6.19 },
    { "text": "The planet’s surface is normally shrouded from sight by a thick layer of clouds.", "start": 6.72, "duration": 6.68 }
  ],
  "description": "🎵 We’ve got it.  On its way to touch the Sun, our Parker Solar Probe took the 1st visible light images of Venus' surface from space! ...",
  "uploader": {
    "id": "11348282",
    "name": "NASA",
    "url": "https://twitter.com/NASA"
  },
  "durationSeconds": 204,
  "uploadDate": "2022-02-09",
  "viewCount": null,
  "isLive": false,
  "thumbnails": [
    { "url": "https://pbs.twimg.com/media/FLKqq-UXoAARGh8.jpg?name=thumb", "width": 150, "height": 150 },
    { "url": "https://pbs.twimg.com/media/FLKqq-UXoAARGh8.jpg?name=orig", "width": 720, "height": 405 }
  ],
  "chapters": [],
  "tags": ["VisionsOfVenus"],
  "extra": {
    "like_count": 3353,
    "comment_count": 75
  },
  "error": null
}
```

`id` is the video's own id on X, which differs from the post id in the link. X
does not show view counts to anonymous visitors, so `viewCount` is usually null.

#### When a post has no transcript

You still get an item, and it costs nothing:

```json
{
  "inputUrl": "https://x.com/CTVJLaidlaw/status/1600649710662213632/video/2",
  "error": {
    "code": "NO_CAPTIONS",
    "message": "This media has no caption tracks.",
    "requestId": "..."
  }
}
```

| Code | Meaning |
| --- | --- |
| `NO_CAPTIONS` | The video has no captions. |
| `UNAVAILABLE` | The post has no video, or it was deleted, made private or withheld. |
| `BLOCKED` | Refused to anonymous viewers: a post marked sensitive, a protected account, or a linked YouTube video behind an age check. |
| `UPSTREAM_RATE_LIMITED` | X is throttling requests. Try again later. |
| `EXTRACTION_FAILED` | A temporary failure. Try again later. |
| `UNSUPPORTED_SITE` | Not an X post, or a post that only links to another website. |
| `INVALID_URL` | Not a valid URL, or a post with several videos: add `/video/1`, `/video/2`. |
| `FREE_PLAN_LIMIT` | Skipped: the Apify free plan's daily transcripts are used up. |
| `CHARGE_LIMIT_REACHED` | Skipped: the run reached the maximum charge you set. |

None of these is charged.

### 🏷️ Pricing

**One price, one event: $1.00 per 1,000 transcripts.** You are charged once per
video that returns a transcript, with or without metadata. Posts without one cost
nothing, and platform usage is included.

| Videos with a transcript | Cost |
| --- | --- |
| 100 | $0.10 |
| 1,000 | $1.00 |
| 10,000 | $10.00 |
| 100,000 | $100.00 |

Set a maximum charge for a run and it stops before going over, listing the URLs
it did not process.

**On the Apify free plan, you get 30 transcripts per day**, across all your runs.
The count resets at 00:00 UTC. Once it is reached, the run stops and returns the
remaining URLs with the `FREE_PLAN_LIMIT` code, free of charge, and the run's
status message says so. Posts without a transcript do not count. Paid Apify plans
have no limit.

### 🔌 Plug it into your AI agent

Through [Apify's MCP server](https://docs.apify.com/platform/integrations/mcp),
this Actor becomes a tool your agent can call. Use this server URL, with your
[Apify API token](https://console.apify.com/settings/integrations):

```
https://mcp.apify.com?tools=fetchrail/x-twitter-transcript
```

| Client | How to add it |
| --- | --- |
| Claude Code | `claude mcp add --transport http apify "https://mcp.apify.com?tools=fetchrail/x-twitter-transcript" --header "Authorization: Bearer YOUR_TOKEN"` |
| Claude Desktop, claude.ai | Add a custom connector with the URL above and sign in to Apify |
| Cursor | In `.cursor/mcp.json`, under `mcpServers`: the URL, plus an `Authorization: Bearer YOUR_TOKEN` header |
| VS Code | In `.vscode/mcp.json`, under `servers`: `"type": "http"`, the URL and the same header |

Then just ask, for example: *"What did they say in this post's video? Give me the
two strongest quotes with timestamps."*

### 🧑‍💻 From code and automations

**One HTTP request.** Runs the Actor and returns the results. Replace `YOUR_TOKEN`
with your Apify API token:

```bash
curl -X POST "https://api.apify.com/v2/acts/fetchrail~x-twitter-transcript/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://x.com/NASA/status/1491475671058681863"], "format": "text"}'
```

**Python**, with `apify-client` version 3 or later:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")
run = client.actor("fetchrail/x-twitter-transcript").call(
    run_input={"urls": ["https://x.com/NASA/status/1491475671058681863"], "format": "text"}
)
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item.get("title"), item.get("transcript") or item["error"])
```

**No code.** Apify's Make, n8n and Zapier integrations run the Actor and pass each
result to your next step, such as a spreadsheet row or a Slack message. Schedules
and webhooks are set up from Apify Console.

### 🙋 Questions people ask

**Why did a video return `NO_CAPTIONS` when it has speech?** X has not made
captions for it and the poster did not upload any. This Actor returns the captions
X already has and does not transcribe audio itself. You are not charged.

**Why did I get a different language than I asked for?** The video has no track
in that language, so the fallback order chose another. Compare `language` with
`preferred` in the result.

**Do `t.co` links work?** No. A `t.co` link can point anywhere, so open it and
paste the `x.com` link it leads to.

**Can I pass a profile, a search or a list?** Not yet. Pass individual post links.

**Is this affiliated with X?** No. It is an independent tool that reads captions X
shows publicly to any viewer.

**Is it legal?** How you use transcripts is your responsibility: they can be
protected by copyright, and spoken content can contain personal data. If in
doubt, consult a lawyer.

### 🛟 Support

Open an issue on this Actor's **Issues** tab. If a post failed, include the
`requestId` from its result, which lets us trace exactly what happened.

# Actor input Schema

## `urls` (type: `array`):

Links to X posts with a video, one per entry. x.com and twitter.com links both work, and so do bare post ids. For a post with several videos, add /video/1, /video/2 and so on to pick one. On the Apify free plan, you get 30 transcripts per day across all runs, reset at 00:00 UTC.

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

A language code such as en, de or pt-BR. A preference, not a filter: a video without a track in it returns English instead, then the first track available. Each result says which language it returned. Leave empty for English where the video has it, otherwise its first track.

## `preferAutomaticCaptions` (type: `boolean`):

Within each language, return X's automatically generated track before one the poster uploaded. Off by default, because uploaded captions are usually more accurate.

## `format` (type: `string`):

Segments with start times and durations, plain text, or a subtitle file (SRT or WebVTT) as text.

## `includeMetadata` (type: `boolean`):

Add the post text, account, video duration, post date, like and reply counts, hashtags and thumbnails to each result, at no extra charge. Off by default, which keeps results small and runs faster.

## Actor input object example

```json
{
  "urls": [
    "https://x.com/NASA/status/1491475671058681863"
  ],
  "preferAutomaticCaptions": false,
  "format": "json",
  "includeMetadata": false
}
```

# Actor output Schema

## `overview` (type: `string`):

One row per URL: title, language, segment count and any error.

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

One row per URL: title, language and the transcript itself, or the error for a post without one.

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

One item per URL with the transcript, the caption tracks the video has and, when asked for, the post's metadata, or an error code for a post without a transcript.

# 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 = {
    "urls": [
        "https://x.com/NASA/status/1491475671058681863"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fetchrail/x-twitter-transcript").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 = { "urls": ["https://x.com/NASA/status/1491475671058681863"] }

# Run the Actor and wait for it to finish
run = client.actor("fetchrail/x-twitter-transcript").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 '{
  "urls": [
    "https://x.com/NASA/status/1491475671058681863"
  ]
}' |
apify call fetchrail/x-twitter-transcript --silent --output-dataset

```

## MCP server setup

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

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/SJrOV95lbjYMEmUOE/builds/odBMjHjVtczSg1fn4/openapi.json
