# YouTube Channel Stats Extractor - Subscribers by Domain (`mambalabs/youtube-channel-transcript-extractor`) Actor

Resolves a company domain or a YouTube handle to the channel and returns the subscriber count, stable channel id, creation date and description. No API key. Flat, Clay ready, one row per company. Transcript pulling is included and is currently gated by YouTube.

- **URL**: https://apify.com/mambalabs/youtube-channel-transcript-extractor.md
- **Developed by:** [Mamba Labs](https://apify.com/mambalabs) (community)
- **Categories:** Lead generation, Automation, Social media
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.45 / 1,000 company checkeds

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/platform/actors/running/actors-in-store#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

### 🔎 What can YouTube Channel Stats Extractor do?

Give it a company domain or a YouTube handle and it returns that company's YouTube channel: subscriber count, the stable channel id, when the channel was created, and its description. One flat row per company, no API key, no login.

Transcript pulling is included as an optional extra and is **currently gated by YouTube**. Read the Known limits section before you turn it on, and note that you are never charged for a transcript that did not arrive.

### 💡 Why use YouTube Channel Stats Extractor?

**Channel stats are the reliable half and they work today.** Subscriber, video and view counts, channel id, creation date and description, resolved from a domain with no API key.

**Transcripts are shipped but currently gated by YouTube, and this listing says so rather than letting you find out.** Measured 2026-08-22: the caption track LIST is reachable, and the caption CONTENT endpoint answers HTTP 200 with an empty body unless the request carries a proof of origin token minted by YouTube's own player. So on most videos today you will get `transcript_status: "not_extractable"`, which means the video has captions and we could not read them. **You are not charged for those.** The transcript event fires only on text actually returned.

**It starts from a domain.** YouTube tools take a channel URL. If you hold a domain list, the channel URL is what you are missing, and YouTube is the easiest platform in this family to resolve: 14 of 14 measured B2B homepages declare their channel.

**It tells you the count is rounded, and it parses the rounding correctly.** YouTube renders "902 thousand subscribers". A parser that only understands K, M and B reads that as 902, a thousandfold undercount, and that exact bug has shipped on real actors. This one converts long scale words first and has a test that would fail if it stopped doing so.

#### 🧭 Why this scrapes rather than using the YouTube API

Decided per field, not as a preference:

- **Channel stats:** scraped. The API needs a key and a daily quota and rounds subscribers exactly the same way the page does, so it buys nothing.
- **Transcripts:** scraped, because there is no alternative. The YouTube Data API does not serve transcript text, and its captions download route requires OAuth as the **channel owner**, which no third party has.
- **Exact subscriber counts:** unavailable from either. YouTube rounds above about a thousand subscribers on the page and in the API.

So the actor needs no API key, and that is a consequence of the field by field answer rather than a design goal.

### 📋 What data can YouTube Channel Stats Extractor extract?

| Field | Type | Meaning |
|---|---|---|
| `degraded` | boolean | True when this row could not be produced normally, for example the company site was unreachable and no discovery could run. A degraded row is never charged. |
| `degradation_reason` | string | null | Why the row is degraded, in plain words. Null on a normal row. |
| `company_domain` | string | null | The company domain this row is about, normalized. Null when only a handle or a name was supplied. This is the join key across the whole Mamba Labs fleet. |
| `company_name` | string | null | The company name as supplied or derived. Improves search accuracy and is what the identity gate matches against. |
| `row_type` | string | channel for the channel summary row, transcript for a per video transcript row. Filter on this before reading any other field: the two row types populate different columns. |
| `youtube_url` | string | null | Canonical YouTube channel URL. |
| `youtube_handle` | string | null | The channel handle, without the leading at sign. |
| `youtube_channel_id` | string | null | The stable UC channel id. A handle can be changed by its owner; this cannot, so store this if you are tracking a channel over time. |
| `youtube_channel_name` | string | null | The channel name as set by its owner. |
| `youtube_subscribers` | integer | null | Subscriber count expanded from what YouTube renders. YouTube rounds every channel above about 1,000 subscribers, so read youtube\_subscribers\_exact before summing. |
| `youtube_subscribers_display` | string | null | Exactly what YouTube rendered, for example "902K". Kept beside the expanded number so the precision is always visible. |
| `youtube_subscribers_exact` | boolean | null | True only below YouTube's rounding floor. False for essentially every real brand channel, because YouTube rounds and there is no exact figure on the page or in its API. |
| `youtube_video_count` | integer | null | Number of public videos on the channel, where the layout declares it. Frequently null: as of 2026-08-22 YouTube's channel layout carries per playlist video counts rather than a channel level one, and reporting a playlist count here would be a wrong number rather than a missing one. |
| `youtube_total_views` | integer | null | Lifetime channel views, where the layout declares them. Frequently null for the same reason as video count: the only view figure on the current layout belongs to one featured video, not to the channel. |
| `youtube_joined_date` | string | null | When the channel was created, as YouTube renders it. |
| `youtube_description` | string | null | The channel about text. |
| `youtube_discovery` | string | null | How the channel was found: input\_handle, homepage\_sameas, homepage\_link, search or pattern\_guess. YouTube is declared on 14 of 14 measured B2B homepages, so this is usually a direct read of the company site. |
| `youtube_status` | string | ok, not\_found, not\_extractable, blocked, identity\_mismatch, auth\_failed or skipped. not\_extractable means the channel loaded and its subscriber count is hidden, which is a real and common owner setting. |
| `video_id` | string | null | On a transcript row, the YouTube video id. Null on a channel row. |
| `video_url` | string | null | On a transcript row, the video watch URL. Null on a channel row. |
| `video_title` | string | null | On a transcript row, the video title. |
| `transcript_text` | string | null | On a transcript row, the full transcript as plain text with no timestamps. This is the field to feed an LLM. |
| `transcript_timestamped` | string | null | On a transcript row, the transcript as newline separated "\[mm:ss] text" lines. Flat text rather than a nested array, so it survives a Clay column and a spreadsheet cell. |
| `transcript_language` | string | null | Language code of the transcript that was pulled. |
| `transcript_is_auto_generated` | boolean | null | True when the transcript is YouTube's automatic speech recognition rather than a human caption track. Auto generated transcripts carry recognition errors and this flag is how you know to expect them. |
| `transcript_status` | string | null | ok, not\_found, not\_extractable, blocked or skipped. not\_found means the video has no caption track at all, which is common and is a real answer. |
| `run_date` | string | ISO 8601 timestamp of this run. Social counts move, so a row without a date is a number with no shelf life. |

**Filter on `row_type` first.** A run with transcripts on returns one `channel` row plus one `transcript` row per video, and the two populate different columns. A channel row has null transcript fields; a transcript row has null channel counts.

### 🛠️ How to pull a company's YouTube channel and transcripts

1. Put a company domain in `company_domain`, or a handle in `handle`.
2. Leave `includeTranscripts` at `false` for channel stats only. That is the cheap, fast configuration and it is what most buyers want.
3. Set it to `true` and set `transcriptVideoCount` to pull transcripts for that many recent videos.
4. For a list, pass an array of objects.

#### 🧪 Using it in Clay

Add an **Enrichment > Apify** column, map `company_domain`.

For channel stats, leave transcripts off and you get one clean row per company. For transcripts, be aware that one input row produces several output rows, which is a one to many shape: run it as its own table rather than as a column on your account table.

### 💵 How much does it cost?

Pay per event. You are charged for output, never for input.

| Event | Fires when | Price |
|---|---|---|
| `company-checked` | Once per company for which the discovery cascade completed and a non degraded channel row was produced, whether or not a channel was found. Does not fire on a degraded row. | $0.0035 |
| `profile-resolved` | Once per company whose candidate channel URL passed the identity gate. Fires on the validation work, not on a populated subscriber cell. | $0.0025 |
| `follower-count-extracted` | Once per channel where a numeric subscriber count was read off the public page. Does not fire when the owner has hidden the count, which reports not\_extractable. | $0.0025 |
| `transcript-extracted` | Once per video for which transcript TEXT was actually returned. A video with no caption track does not charge, and neither does a video whose caption track exists but whose content YouTube refused to serve. Never fires when transcripts are turned off. This is deliberately strict: the fetch costs us roughly a cent whether or not it yields text, and the buyer should not carry that. | $0.0150 |

Transcripts are billed per video that actually returned text. A video with no caption track costs you nothing, because nothing was delivered.

`transcriptVideoCount` is the dial that sets the cost of a transcript run: five videos is five transcript events.

**What the same coverage costs bought a la carte:** streamers/youtube-channel-scraper charges $0.0013 per result for channel and video inventory and takes a channel URL. Dedicated transcript actors on the Store sit in the $0.001 to $0.005 per video range. This actor is priced in the same band and adds domain to channel resolution, which neither does.

### ⌨️ Input

| Field | Type | Required | Meaning |
|---|---|---|---|
| `company_domain` | string | no | Bare company domain, for example shopify.com. Supply this or a handle. With a domain the actor runs full discovery; with a handle it skips straight to the fetch. |
| `company_name` | string | no | Optional. Improves search accuracy and is what the identity gate checks a discovered profile against, so supplying it reduces wrong matches. |
| `handle` | string | no | Optional. A YouTube handle such as @shopify, or a full channel URL. Supplying it skips discovery and goes straight to the channel. |
| `includeFollowerCounts` | string | no | When "true" (default) the profile page is fetched and the counts are extracted. Set "false" to resolve the profile URL only, which is cheaper and needs no proxy. Sent as a string for Clay compatibility. |
| `skipCache` | string | no | When "false" (default) a successful lookup is cached for seven days and reused. Set "true" to force a fresh fetch. Sent as a string for Clay compatibility. |
| `includeTranscripts` | string | no | When "true", full timestamped transcripts are pulled for the most recent videos on the channel and returned as one row per video. Default is "false" because transcripts are the expensive half of this actor and most buyers want channel stats only. Sent as a string for Clay compatibility. |
| `transcriptVideoCount` | string | no | How many of the channel's most recent videos to pull transcripts for, when transcripts are on. Default 5. Each video is one extra fetch and one extra billable transcript event, so this is the dial that sets the cost of a transcript run. |

```json
{
  "company_domain": "shopify.com",
  "company_name": "Shopify",
  "includeTranscripts": "false"
}
```

### 📤 Output

One flat, snake\_case row per company. No nested objects, so it drops straight
into Clay, a spreadsheet or a warehouse table without a flattening step.

```json
{
  "degraded": false,
  "degradation_reason": null,
  "company_domain": "shopify.com",
  "company_name": "Shopify",
  "row_type": "channel",
  "youtube_url": "https://www.youtube.com/@shopify",
  "youtube_handle": "shopify",
  "youtube_channel_id": "UCIv38OrggTu3vNkCAo96-CQ",
  "youtube_channel_name": "Shopify",
  "youtube_subscribers": 902000,
  "youtube_subscribers_display": "902K",
  "youtube_subscribers_exact": false,
  "youtube_video_count": null,
  "youtube_total_views": null,
  "youtube_joined_date": "Aug 22, 2008",
  "youtube_description": "Shopify is the leading global commerce company.",
  "youtube_discovery": "homepage_link",
  "youtube_status": "ok",
  "video_id": null,
  "video_url": null,
  "video_title": null,
  "transcript_text": null,
  "transcript_timestamped": null,
  "transcript_language": null,
  "transcript_is_auto_generated": null,
  "transcript_status": null,
  "run_date": "2026-08-22T09:00:00.000Z"
}
```

#### false versus null, and why the difference matters

`false` means we looked and the answer is no. `null` means we could not look,
or the platform withheld it. They are never interchangeable in this output. If
you filter for companies with no presence on this platform, filter on `false`,
because `null` rows are unknown rather than absent and including them will
overstate your list.

### 💡 Tips

- **Leave transcripts off unless you need the text.** The channel page is 1.9 MB and the transcripts are extra fetches on top.
- **Store `youtube_channel_id`,** not the handle. Handles can be changed by their owner.
- **`transcript_is_auto_generated: true` means expect recognition errors,** especially on product names and jargon. Worth knowing before you feed it to a model.
- **Use `transcript_text` for LLM input and `transcript_timestamped` for humans** who need to find the moment.

### ⚠️ Known limits

- **Subscriber counts are rounded by YouTube** above roughly a thousand subscribers, on the page and in the official API alike. There is no exact figure to be had. `youtube_subscribers_exact` marks it.
- **Transcript extraction is currently gated by YouTube and mostly returns `not_extractable`.** This is the headline limitation. Measured 2026-08-22 across every available route: the caption track list is readable over residential routing, and the caption content endpoint returns HTTP 200 with a zero byte body without a proof of origin token minted by YouTube's player. The actor distinguishes this from a video that genuinely has no captions, and charges for neither.
- **Not every video has a transcript.** Many have no caption track at all. That returns `transcript_status: "not_found"`, which is a real answer about the video, and it is not charged.
- **Auto generated transcripts contain recognition errors.** The flag tells you which ones they are.
- **Channels can hide their subscriber count.** That is an owner setting and it reports `not_extractable`, not zero.
- **The channel page is the heaviest fetch in this actor family** at 1.9 MB to 3.5 MB. It is cheap in money over datacenter routing and it is not fast.
- **No comments, no per video engagement metrics, no watch time.**
- **Channel level video count and total views are frequently null.** YouTube's current channel layout publishes per playlist video counts and a single featured video's view count, neither of which is a channel total. This actor returns null rather than reporting one of those as if it were the channel figure.

### ❓ FAQ

**Does this need a YouTube API key?**
No. And the API would not help: it does not serve transcript text at all, and it rounds subscriber counts exactly as the page does.

**Why can I not get an exact subscriber count?**
YouTube rounds it everywhere it publishes it. Anyone offering you an exact figure is either a channel owner or guessing.

**Why do I get several rows for one company?**
Because transcripts are on. One `channel` row plus one `transcript` row per video. Filter on `row_type`.

**What if a video has no captions?**
`transcript_status` is `not_found` and you are not charged for that video.

**What is the difference between `not_found` and `not_extractable` on a transcript?**
`not_found` means the video genuinely has no caption track. `not_extractable` means it HAS one and YouTube would not serve us the text. The distinction matters: a tool that reported both as "no transcript" would tell you a captioned video has no captions. Neither is charged.

**Can I pass a handle or a channel URL instead of a domain?**
Yes, in `handle`.

### 🧩 Want other GTM data?

Mamba Labs builds a fleet of GTM enrichment actors that share one flat,
Clay-ready output convention, so their rows join on `company_domain` with no
cleaning step:

- **Company Firmographic Enricher** headcount, industry, location and revenue band from a domain, joins on `company_domain`.
- **Domain to LinkedIn URL Resolver** the company LinkedIn URL for domains where the social mapper found none.
- **GTM Hiring Signal Scraper** open go to market roles, which is the buying signal a follower count is a proxy for.
- **Company Social Presence Mapper** every platform in one row when you want breadth rather than depth on one network.

Full fleet: [apify.com/mambalabs](https://apify.com/mambalabs)

### 🆘 Support

Issues, field requests and bug reports: open an issue on the actor's Issues tab.
Mamba Labs reads every one.

# Actor input Schema

## `company_domain` (type: `string`):

Bare company domain, for example shopify.com. Supply this or a handle. With a domain the actor runs full discovery; with a handle it skips straight to the fetch.

## `company_name` (type: `string`):

Optional. Improves search accuracy and is what the identity gate checks a discovered profile against, so supplying it reduces wrong matches.

## `handle` (type: `string`):

Optional. A YouTube handle such as @shopify, or a full channel URL. Supplying it skips discovery and goes straight to the channel.

## `includeFollowerCounts` (type: `string`):

When "true" (default) the profile page is fetched and the counts are extracted. Set "false" to resolve the profile URL only, which is cheaper and needs no proxy. Sent as a string for Clay compatibility.

## `skipCache` (type: `string`):

When "false" (default) a successful lookup is cached for seven days and reused. Set "true" to force a fresh fetch. Sent as a string for Clay compatibility.

## `includeTranscripts` (type: `string`):

When "true", full timestamped transcripts are pulled for the most recent videos on the channel and returned as one row per video. Default is "false" because transcripts are the expensive half of this actor and most buyers want channel stats only. Sent as a string for Clay compatibility.

## `transcriptVideoCount` (type: `string`):

How many of the channel's most recent videos to pull transcripts for, when transcripts are on. Default 5. Each video is one extra fetch and one extra billable transcript event, so this is the dial that sets the cost of a transcript run.

## `source_tag` (type: `string`):

Internal attribution tag set by Mamba Labs on published task examples. Not required, and nothing depends on it. Leave it empty.

## Actor input object example

```json
{
  "company_domain": "shopify.com",
  "company_name": "Shopify",
  "includeFollowerCounts": "true",
  "skipCache": "false",
  "includeTranscripts": "false",
  "transcriptVideoCount": "5"
}
```

# Actor output Schema

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

Dataset of one flat row per company, with per platform status so a blocked fetch never reads as a zero.

# 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 = {
    "company_domain": "shopify.com",
    "company_name": "Shopify",
    "handle": "",
    "transcriptVideoCount": "5"
};

// Run the Actor and wait for it to finish
const run = await client.actor("mambalabs/youtube-channel-transcript-extractor").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 = {
    "company_domain": "shopify.com",
    "company_name": "Shopify",
    "handle": "",
    "transcriptVideoCount": "5",
}

# Run the Actor and wait for it to finish
run = client.actor("mambalabs/youtube-channel-transcript-extractor").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 '{
  "company_domain": "shopify.com",
  "company_name": "Shopify",
  "handle": "",
  "transcriptVideoCount": "5"
}' |
apify call mambalabs/youtube-channel-transcript-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mambalabs/youtube-channel-transcript-extractor"
        }
    }
}

```

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/6hSpD3kd2s2IQ4VSs/builds/I5gTOWFPrEZZSakaS/openapi.json
