# Podcast Transcript Scraper 🎙️ With Speaker Labels (`cleanfeed/podcast-transcript-downloader`) Actor

Download podcast transcripts published by the shows themselves, with millisecond timestamps and speaker labels. Pass a podcast name or RSS feed; get full text, timed segments, speaker count and episode metadata as JSON or CSV. Episodes with no published transcript are reported, never charged.

- **URL**: https://apify.com/cleanfeed/podcast-transcript-downloader.md
- **Developed by:** [Yaniv van der Stigchel](https://apify.com/cleanfeed) (community)
- **Categories:** Videos, AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.48 / 1,000 transcript returneds

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

## Podcast Transcript Scraper 🎙️ with speaker labels

Transcripts that podcast publishers provide themselves, in the open Podcasting
2.0 standard. Millisecond timestamps and speaker labels — the two things video
captions almost never have.

### What it does

- Download podcast transcripts in bulk
- Get timestamped segments with speaker labels
- Build a searchable archive of a show's back catalogue
- Export podcast transcripts to JSON or CSV
- Feed spoken-word text into a RAG pipeline or a language model

### Why these are better than video captions

Publishers ship these deliberately, produced properly rather than guessed from
audio:

```
00:00:00,240 --> 00:00:02,880
Speaker 1: Please note that this show discusses...
```

Two things come free that automatic video captions do not give you:

- **Speaker labels.** Diarisation is expensive to compute and here it is already
  done. `hasSpeakerLabels` and `speakerCount` are on every row.
- **Real punctuation.** These are edited files, not a stream of words.

### The honest number

**Roughly half of podcast feeds publish a transcript.** Measured across 30 feeds
sampled from eight genres on 4 September 2026, 14 of 30 — 46.7% — carried the
`<podcast:transcript>` tag, at 114 to 171 tagged episodes per show.

The other half publish none, and no tool can extract text that was never
created. Those episodes come back as `no-transcript-published` and are never
charged. Anything claiming complete coverage of all podcasts is either running
speech recognition — a different and far more expensive product — or is wrong.

Sample size is 30, so treat 46.7% as a first reading rather than a settled
figure.

### You only pay for transcripts

| `errorCode` | Meaning | Charged |
|---|---|---|
| — (`success: true`) | A transcript | Yes |
| `no-transcript-published` | Publisher provides none | No |
| `transcript-unreachable` | Link published but the file would not fetch | No |
| `transcript-empty` | File fetched but had no readable cues | No |
| `feed-unreachable` | Feed did not respond or is not valid RSS | No |
| `show-not-found` | No podcast matched that name | No |
| `no-episodes` | Feed is valid but empty | No |

### Input

| Field | Required | Description |
|---|---|---|
| `shows` | yes | Podcast names or RSS feed URLs |
| `maxEpisodesPerShow` | no | Newest first. Default 50. Your cost ceiling |
| `language` | no | Two-letter code. A filter, not a preference |
| `includeSegments` | no | Adds timed cues. Default false |
| `maxConcurrency` | no | 1–15, default 5 |

```json
{
  "shows": ["This Podcast Will Kill You", "https://feeds.megaphone.fm/vergecast"],
  "maxEpisodesPerShow": 100,
  "includeSegments": true
}
```

A feed URL skips the directory lookup, so it is faster than a name.

### Output

Every row has the same fields whether it succeeded or failed, so you can select
columns without branching.

| Field | Type | Description |
|---|---|---|
| `success` | boolean | True when the row carries a transcript |
| `sourceShow` | string | The name or feed you supplied |
| `showTitle` | string | The show's own title |
| `episodeTitle` | string | Episode title |
| `episodeGuid` | string | Feed's unique id — deduplicates across runs |
| `episodeUrl` | string | Episode page, where published |
| `publishedAt` | string | Publication date |
| `seasonNumber` | integer | Where published |
| `episodeNumber` | integer | Where published |
| `durationSeconds` | number | From the feed, or the transcript's own span |
| `transcriptFormat` | string | MIME type the publisher provided |
| `language` | string | As the publisher declared it |
| `hasSpeakerLabels` | boolean | True when speakers are named |
| `speakerCount` | integer | Distinct speakers labelled |
| `segmentCount` | integer | Timed cues — always reported |
| `fullText` | string | Whole transcript as plain text |
| `segments` | array | Timed cues with speakers; null unless requested |
| `errorCode` | string | Null on success |
| `errorMessage` | string | Human-readable explanation |

```json
{
  "success": true,
  "sourceShow": "This Podcast Will Kill You",
  "episodeTitle": "Ep 141: Scurvy",
  "durationSeconds": 3723,
  "transcriptFormat": "application/srt",
  "hasSpeakerLabels": true,
  "speakerCount": 3,
  "segmentCount": 842,
  "fullText": "Hi. My name's Elan. I'm a nurse practitioner...",
  "errorCode": null,
  "errorMessage": null
}
```

### Use it from an AI agent (MCP)

This Actor is callable as a tool through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp), so Claude, ChatGPT, Cursor and VS Code can run it directly.

Add the server to your MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com",
      "headers": {
        "Authorization": "Bearer <YOUR_APIFY_TOKEN>"
      }
    }
  }
}
```

Then ask for what you want in plain language — for example *“get the transcripts for this podcast”* — and the agent calls `cleanfeed/podcast-transcript-downloader` with the right input. Every output field is described in the dataset schema, so the agent knows what it is getting back before it runs anything.

### Call it from code

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("cleanfeed/podcast-transcript-downloader").call(run_input={
    "shows": ["This Podcast Will Kill You"],
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["success"]:
        print(item)
```

#### JavaScript

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });

const run = await client.actor('cleanfeed/podcast-transcript-downloader').call({
    shows: ["This Podcast Will Kill You"],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.success));
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/cleanfeed~podcast-transcript-downloader/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"shows": ["This Podcast Will Kill You"]}'
```

### Related actors

| If you need | Use |
|---|---|
| Transcripts from YouTube videos instead | [YouTube Transcript Scraper](https://apify.com/cleanfeed/youtube-transcript-downloader) |
| Transcripts for a whole YouTube channel | [YouTube Channel Transcript Scraper](https://apify.com/cleanfeed/youtube-channel-transcript-downloader) |

### Limitations

- **Roughly half of feeds publish no transcript.** Measured across 30 feeds in eight genres on 4 September 2026, 14 of 30 carried the `<podcast:transcript>` tag. The rest return `errorCode: no-transcript-published` and are never charged. Sample size is 30, so treat 46.7% as a first reading.
- **No speech-to-text fallback.** Transcripts are read, not generated. Recognition costs real money per hour of audio and would change the price by a large multiple.
- **Transcripts are the publisher's work and remain theirs.** Quote with attribution; do not republish an episode's transcript as your own content.
- **Quality varies with who produced it.** Some are professionally edited, some are lightly-corrected machine output. The format and speaker count are reported so you can judge; the text is never rewritten.
- **Speaker labels are as the publisher wrote them**, often "Speaker 1" rather than a real name, with no published mapping.
- **Transcript links point at the publisher's host**, not ours. A dead link is reported as `transcript-unreachable` rather than hidden.

### FAQ

#### Why does this show return nothing?

Most likely the publisher does not include `<podcast:transcript>` in its feed. Around half do not. You get one row per episode saying so, at no charge.

#### Can you transcribe the audio when there is no transcript?

Not in this Actor. Speech recognition costs real money per hour of audio and would change the price by a large multiple. Shipping it silently inside a per-transcript price would be dishonest about what you are paying for.

#### What is the difference from a YouTube transcript scraper?

The source. YouTube gives automatic captions, which have no speaker labels and unreliable punctuation. These are files the publisher produced, usually with both.

#### Does it work for any podcast?

Any podcast with a public RSS feed, which is nearly all of them. Shows exclusive to one platform with no public feed cannot be reached.

#### Do I get speaker names?

You get the labels the publisher wrote. Many use `Speaker 1` and `Speaker 2` rather than real names, and no mapping is published. `hasSpeakerLabels` and `speakerCount` tell you what you have before you read the text.

#### Can I pass an RSS feed directly?

Yes, and it is faster — a feed URL skips the podcast-directory lookup entirely.

### Notes

Only transcripts that publishers link from their own RSS feeds are downloaded,
via the open Podcasting 2.0 `<podcast:transcript>` tag. No audio is processed
and no transcript is generated — if a publisher provides none, that is reported.

# Actor input Schema

## `shows` (type: `array`):

Podcast names or RSS feed URLs. A name is resolved through Apple's public podcast directory; a feed URL is used directly and is faster.

## `maxEpisodesPerShow` (type: `integer`):

Newest first. You are charged per transcript returned, so this is your cost ceiling. Episodes with no transcript are reported and never charged.

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

Two-letter code such as en or de. A filter, not a preference: if a show publishes transcripts only in another language, you get an honest empty result rather than the wrong language.

## `includeSegments` (type: `boolean`):

Adds every timed cue with its speaker label alongside the full text. Off by default because it is large; segmentCount is always reported either way.

## `maxConcurrency` (type: `integer`):

Shows processed in parallel.

## Actor input object example

```json
{
  "shows": [
    "This Podcast Will Kill You",
    "Betrayal"
  ],
  "maxEpisodesPerShow": 50,
  "includeSegments": false,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

One row per episode: full text, speaker labels, timestamps, and episode metadata.

## `summary` (type: `string`):

Shows processed, transcripts delivered, and how many episodes publish none.

# 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 = {
    "shows": [
        "This Podcast Will Kill You",
        "Betrayal"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanfeed/podcast-transcript-downloader").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 = { "shows": [
        "This Podcast Will Kill You",
        "Betrayal",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("cleanfeed/podcast-transcript-downloader").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 '{
  "shows": [
    "This Podcast Will Kill You",
    "Betrayal"
  ]
}' |
apify call cleanfeed/podcast-transcript-downloader --silent --output-dataset

```

## MCP server setup

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

```

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/Lrq7SMnjNER6JCXAX/builds/tTYo0HWisoeyXLEcb/openapi.json
