# Telegram Channel Content & Media Scraper (`webfinity/telegram-channel-content-media-scraper-v2`) Actor

Need data from Telegram channels without an API key or phone number? Paste up to 25 channels and get clean JSON: post text, views, reactions, forwards, hashtags, mentions, links and photos/videos with direct download links. Great for monitoring competitors, news and AI datasets. See README.

- **URL**: https://apify.com/webfinity/telegram-channel-content-media-scraper-v2.md
- **Developed by:** [Olga](https://apify.com/webfinity) (community)
- **Categories:** Social media, Automation
- **Stats:** 1,171 total users, 82 monthly users, 99.8% runs succeeded, 27 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

$10.00 / 1,000 posts

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?

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

### 📦 Telegram Channel Content & Media Scraper

Extract channel metadata, recent posts, views, reactions, forwards, hashtags, mentions, replies, and all media with **direct download links**.
This actor scrapes structured content from **public Telegram channels** using the Telegram API via a **pre-authorized session string** — no setup required from you.

> 🆕 **Updated on September 29, 2026:** up to 25 channels per run, fewer Telegram rate limits, album merging, clear run status and a run summary — live now.
>
> 💰 **From October 13, 2026:** the price stays the same — $10 per 1,000 posts — but you will no longer pay for service records such as "no posts found" or channel errors. Details below in **Update** and **Pricing**.

***

### 🆕 Update (2026-09-29)

- ✅ **Channels are remembered between runs** — channels you've scraped before no longer need a Telegram username lookup (the thing that causes 10-hour FloodWait limits). Repeat monitoring of the same channels is faster and much safer
- ✅ **Up to 25 channels per run** (was 10) — of them, up to 10 *new* channels; previously scraped channels don't count toward that limit
- ✅ **Rate limits no longer stop the whole run** — if Telegram limits lookups of new channels, only new channels are skipped; channels you've scraped before keep working
- ✅ **Clear run status** — while waiting in the queue you see how long you've been waiting; at the end you see a summary: posts collected, media files downloaded, which channels were not processed and why
- ✅ **`RUN_SUMMARY`** record in the run's Key-Value Store — handy for API and automation users
- ✅ **`type` field on every record** — `post` for content, other values for service messages (see Output), so you can filter them out easily
- ✅ **`t.me` links accepted** — `https://t.me/channel`, `https://t.me/s/channel`, `t.me/channel/123`; channels can be separated by commas or new lines
- ✅ **Optional album merging** (`groupAlbums`) — merge an album into one record with all its files
- ✅ **`maxMediaSizeMB`** — files larger than the limit stay as links instead of being downloaded
- ✅ **Link previews are no longer treated as media** — `hasMedia` now means real photos, videos or files

Nothing changes in the default output: without the new options, you get the same records as before, plus the `type` and `groupedPostIds` fields.

***

### 💰 Pricing

| What | Price |
|---|---|
| Post | **$10.00 / 1,000 posts** |
| Platform usage | **Included** |

That's it — no monthly fee, no run fee, no extra charge for downloading media.

> 📢 **From October 13, 2026 — same price, fairer billing.** Until now, every record in the dataset was charged, including service records. From October 13 you pay **only for real posts**. These become free:
>
> - service records: "no posts found", channel errors, rate-limit notices, skipped channels;
> - time in the queue, queue timeouts and failed connections to Telegram (these were never charged).
>
> With `groupAlbums: true`, an album is one post — you pay once per album, not once per photo.

Set **Max cost per run** in the run options to cap your spending — the Actor stops cleanly when the limit is reached.

***

#### ✅ What This Actor Does

- Connects to Telegram automatically (no credentials needed from your side)
- Parses public channels (by username or `t.me` link)
- Retrieves for each channel:
  - **Channel metadata**: display title, description (about), subscriber count
  - **Posts**: text, date, post URL, views, reactions (with emoji breakdown + total)
  - **Forwards**: detects native Telegram forwards and exposes the source (`isForward`, `forwardFromType`, `forwardFromId`, `forwardFromName`, `forwardChannelPost`, `forwardPostAuthor`, `forwardDate`)
  - **Hashtags & mentions**: extracted from post text (Unicode-aware — supports Cyrillic, e.g. `#вакансия`, `#Менеджер_по_продажам`)
  - **Edit metadata**: `editDate` if the post was edited
  - **Reply chain**: `replyToMsgId` and `replyToPostUrl` for posts that reply to another post
  - **Links**: all URLs found in the post text
  - **Media**: photos, videos, documents — with direct downloadable URLs (`.jpg`, `.mp4`, `.pdf`, etc.) and `mediaTypes`
- Handles:
  - Text-only and media-only messages
  - Albums — one record per message by default, or one record per album with `groupAlbums`
  - Telegram FloodWait limits gracefully (with explanatory record in the dataset)
  - Cross-run conflicts via a per-session queue (see "Queue Behavior" below)
- Skips:
  - Empty service messages, outdated posts beyond `daysRange`
- Starts from the **most recent post** and works backwards

***

### 🧾 Input

You don't need to enter any credentials — they're already preconfigured.

| Field            | Type    | Required | Default | Description |
|------------------|---------|----------|---------|-------------|
| `channels`       | string  | ✅       | —       | Channel usernames or links, separated by commas or new lines (e.g. `@channel1, https://t.me/channel2`) |
| `maxPosts`       | number  | ❌       | 10      | Max number of Telegram messages per channel (1–200) |
| `daysRange`      | number  | ❌       | 3       | Number of days to look back from today (1–30) |
| `includeText`    | boolean | ❌       | true    | Whether to include post text in the output |
| `mediaOnly`      | boolean | ❌       | false   | If `true`, skip posts that have no photos, videos or files |
| `downloadMedia`  | boolean | ❌       | false   | If `true`, download media files into the run's Key-Value Store |
| `maxMediaSizeMB` | number  | ❌       | 50      | Files larger than this stay as `t.me` links (not downloaded). `0` = no limit |
| `groupAlbums`    | boolean | ❌       | false   | If `true`, merge an album into one record: all files in `mediaUrls`, all message ids in `groupedPostIds`. With merging on you may get fewer records than `maxPosts`, because `maxPosts` counts Telegram messages |

📌 **Example input**

```json
{
  "channels": "@channel1, https://t.me/channel2, channel3",
  "maxPosts": 10,
  "daysRange": 3,
  "includeText": true,
  "mediaOnly": false,
  "downloadMedia": true,
  "maxMediaSizeMB": 50,
  "groupAlbums": false
}
```

> ⚠️ **Channel limits per run.** Up to **25 channels** per run, of which up to **10 new channels** (not scraped by this Actor before). Previously scraped channels are remembered and don't need a Telegram lookup, so they don't count toward the 10. Channels over the limit are listed in the run status and saved as `type: "skipped"` records — just run them again.

***

### ▶️ How to Run

1. Open the actor in **Apify Console**
2. Fill in the input fields
3. Click **Save**, then **Run**
4. Watch the **run status** — it shows queue waiting time, progress, and a final summary
5. Results are available in:
   - **Dataset** — all posts and channel metadata as JSON records
   - **Key-Value Store** — downloaded media files (when `downloadMedia: true`) and `RUN_SUMMARY`

***

### 📤 Output

Each Telegram message (or each album, with `groupAlbums: true`) is saved as a separate record in the default dataset (JSON format). Every record has a `type` field:

| `type` | Meaning |
|---|---|
| `post` | A post with content |
| `no_posts` | Channel has no posts in the selected period |
| `skipped` | Channel postponed (per-run limit or temporary Telegram limit) — run it again |
| `telegram_rate_limit` | Telegram asked to slow down; includes `waitSeconds` |
| `channel_error` | Channel not found, private, or another error |

To get only content, filter by `type = "post"`.

```json
{
  "type": "post",
  "channel": "@channel_name",
  "channelLink": "https://t.me/channel_name",
  "channelTitle": "Channel Display Name",
  "channelDescription": "Channel about / bio text",
  "postId": 101,
  "postUrl": "https://t.me/channel_name/101",
  "date": "2026-09-28T17:38:00.000Z",
  "views": 3456,
  "subscribers": 12345,
  "text": "Example post content with #hashtag and @mention",
  "reactions": [
    { "emoji": "👍", "count": 42 },
    { "emoji": "❤️", "count": 18 }
  ],
  "totalReactions": 60,
  "links": ["https://example.com"],
  "hasMedia": true,
  "mediaUrls": [
    "https://api.apify.com/v2/key-value-stores/<storeId>/records/channel_name_101.jpg?disableRedirect=1"
  ],
  "mediaTypes": ["jpg"],
  "groupedPostIds": [101],
  "isForward": false,
  "forwardFromType": "",
  "forwardFromId": "",
  "forwardFromName": "",
  "forwardChannelPost": 0,
  "forwardPostAuthor": "",
  "forwardDate": "",
  "hashtags": ["#hashtag"],
  "mentions": ["@mention"],
  "editDate": "",
  "replyToMsgId": 0,
  "replyToPostUrl": ""
}
```

For forwarded posts, `isForward: true` and the `forward*` fields are populated:

```json
{
  "isForward": true,
  "forwardFromType": "channel",
  "forwardFromId": "1009232144",
  "forwardFromName": "",
  "forwardChannelPost": 181471,
  "forwardPostAuthor": "",
  "forwardDate": "2026-04-27T15:06:48.000Z"
}
```

**`RUN_SUMMARY`** (in the run's Key-Value Store):

```json
{
  "posts": 48,
  "channelsProcessed": 5,
  "mediaDownloaded": 31,
  "mediaSkippedLarge": 1,
  "postponedChannels": [],
  "failedChannels": [],
  "costLimitReached": false
}
```

***

### ⏳ Queue Behavior (Important for parallel users)

The actor uses a **shared Telegram session** preconfigured by the author. Telegram allows only one active connection per session at any time — if multiple users run the actor simultaneously, the second connection would get `AUTH_KEY_DUPLICATED`.

To prevent this, the actor uses a **cross-run lock**: parallel runs are automatically queued and processed one at a time.

**What this means in practice:**

- If no one else is running the actor → starts immediately
- If another user is currently scraping → your run **waits in queue**, and the **run status** shows how long you've been waiting
- A typical wait is **a few minutes** (depends on what the previous run is doing)
- Maximum queue wait is **15 minutes**, after which the run fails with a clear message

***

### 🛡️ Telegram Rate Limit Handling

If Telegram returns a **FloodWait** on `contacts.ResolveUsername` (too many lookups of new channel usernames in a short period), the actor:

1. Stops looking up **new** channels for the rest of this run, to protect the session
2. **Keeps processing channels you've scraped before** — they don't need a lookup
3. Adds a `telegram_rate_limit` record with the wait duration (`waitSeconds`) and a clear explanation
4. Retry the skipped channels after the indicated time (often 10–11 hours)

For any other very long FloodWait (1 hour or more), the run stops early to protect the session and reports it in the dataset and run status.

***

### 🛠 Use Cases

- Competitive content monitoring
- Telegram market research
- Dataset building for AI/ML models (with hashtags + mentions for labels)
- Public media archiving
- Social listening and trend analysis
- News aggregation and curation
- Forward chain analysis (track which posts get republished)

***

### 📜 Changelog

**2026-09-29**

- ✅ Billing announced for 2026-10-13: same $10 / 1,000 posts, service records no longer charged
- ✅ Channel cache: previously scraped channels skip `contacts.ResolveUsername` (the main cause of FloodWait); each channel is now resolved once per run instead of twice
- ✅ Up to 25 channels per run, max 10 new ones; channels over the limit are reported as `type: "skipped"` instead of being silently dropped
- ✅ FloodWait on new-channel lookups no longer stops already-known channels
- ✅ Run status messages: queue waiting time, progress per channel, final summary
- ✅ `RUN_SUMMARY` record in the Key-Value Store
- ✅ `type` field on every record (`post`, `no_posts`, `skipped`, `telegram_rate_limit`, `channel_error`)
- ✅ `channels` accepts `t.me` links (incl. `/s/` and post links) and new-line separation; duplicates removed
- ✅ New `groupAlbums` option (off by default) and `groupedPostIds` field
- ✅ New `maxMediaSizeMB` option (default 50 MB)
- ✅ Link previews no longer counted as media (`hasMedia`, `mediaUrls`)
- ✅ Respects the *Max cost per run* limit
- ✅ Clearer error messages for invalid, private and non-channel usernames

**2026-05-03**

- ✅ Added channel metadata (`channelTitle`, `channelDescription`)
- ✅ Added forwards detection (7 new `forward*` fields)
- ✅ Added hashtags + mentions extraction
- ✅ Added `editDate` and reply chain fields (`replyToMsgId`, `replyToPostUrl`)
- ✅ Cross-run lock now uses author-scoped Apify token, so the queue works for all users (not only the actor owner)
- ✅ Replaced silent exit handling with explicit `Actor.fail()` so errors are visible in run status
- ✅ Added 15-second timeout on Telegram `getMe()` to prevent multi-minute hangs on dead sessions
- ✅ Disabled `autoReconnect` to fail fast and surface real errors

**2025-12-05**

- ✅ Added explicit handling and logging for Telegram `contacts.ResolveUsername` FloodWait errors
- ✅ When Telegram returns a FloodWait, the actor now stops processing further channels in that run and adds a `telegram_rate_limit` record to the dataset with a human-readable explanation
- ✅ Limited the number of channels processed per run to **10** to reduce the risk of hitting global FloodWait on the shared Telegram session
- ✅ Updated input description to recommend no more than 10 channels per run and documented this behavior in the README

**2025-10-10**

- ✅ Fixed `AUTH_KEY_DUPLICATED` by adding a **per-session lock** (prevents concurrent reuse of the same Telegram auth key)
- ✅ Improved error messages for session conflicts
- ✅ Internal stability tweaks for Telegram requests and media downloads

***

### ❓ Troubleshooting

**Empty results / `type: "no_posts"`**

- Increase `daysRange` to `7..14` — the channel may not have posts in the default 3-day window
- Increase `maxPosts` to `20..50`
- Make sure the channel is **public** and the username is correct

**`type: "skipped"` records / channels "not processed" in the run status**

- The run hit the per-run channel limits — run again with just those channels

**`type: "channel_error"`**

- The channel may be private, deleted, a user or bot rather than a channel, or an invite link (`t.me/+...`), which isn't supported

**Run waits 5+ minutes in queue**

- Another user is currently using the actor; this is normal under load
- Retry in a few minutes when the queue clears

**`telegram_rate_limit` record in dataset**

- Telegram applied a FloodWait — wait the indicated number of hours before retrying the skipped channels
- Channels you've scraped before are not affected

**Run failed with `AUTH_KEY_DUPLICATED`**

- Should not happen with the built-in queue, but if it does — wait 10 minutes and retry

***

### ⚠️ Telegram Albums

Telegram delivers an album (several photos/videos with one caption) as **several separate messages**, one file each, with the caption on one of them.

**By default** (`groupAlbums: false`) the actor returns them as Telegram delivers them — one record per message. They share the same `date` and have consecutive `postId` values.

**With `groupAlbums: true`** the actor merges them into one record:

- `text` — the album's caption
- `mediaUrls` / `mediaTypes` — all files of the album
- `groupedPostIds` — ids of all messages in the album
- `postId` / `postUrl` — the first message of the album

***

### 🔐 License & Usage

This actor is a **proprietary paid product**.

- ❗ It is **not open source**
- 📦 It is licensed for **private and commercial use** by users of this Actor
- 🛑 Any unauthorized copying, reselling, or redistribution is strictly prohibited
- 🧑‍💻 All activity is logged for abuse prevention and fair use enforcement

To report a bug, please open an issue — I respond quickly.

***

Made with 💙 for Telegram research & automation.

# Actor input Schema

## `channels` (type: `string`):

Channel usernames or links, separated by commas or new lines. Accepted: channel, @channel, https://t.me/channel, https://t.me/s/channel. Up to 25 channels per run, of which up to 10 new ones (channels scraped before don't count toward the 10).

## `maxPosts` (type: `integer`):

Maximum number of recent posts to fetch from each channel. Keep small for faster runs.

## `daysRange` (type: `integer`):

Number of past days to include when filtering posts by date.

## `includeText` (type: `boolean`):

Whether to include the text content of each post in the results.

## `mediaOnly` (type: `boolean`):

If true, only return posts that contain photos, videos or files.

## `downloadMedia` (type: `boolean`):

If true, download media files to the run's Key-Value Store and include direct URLs in results. If false, media is returned as t.me links.

## `maxMediaSizeMB` (type: `integer`):

Files larger than this are not downloaded and stay as t.me links. 0 = no limit.

## `groupAlbums` (type: `boolean`):

Telegram sends an album as several messages, one file each. Off (default): one record per message, as before. On: one record per album with all files in mediaUrls.

## Actor input object example

```json
{
  "channels": "durov, telegram",
  "maxPosts": 10,
  "daysRange": 3,
  "includeText": true,
  "mediaOnly": false,
  "downloadMedia": false,
  "maxMediaSizeMB": 50,
  "groupAlbums": false
}
```

# Actor output Schema

## `posts` (type: `string`):

All scraped posts with views, reactions, forwards, hashtags, links and media URLs. Filter by type = "post" to skip service records.

## `mediaFiles` (type: `string`):

Photos, videos and documents saved when downloadMedia is on.

## `runSummary` (type: `string`):

Posts collected, media downloaded, and channels that were skipped or failed, with reasons.

# 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 = {
    "channels": "durov, telegram"
};

// Run the Actor and wait for it to finish
const run = await client.actor("webfinity/telegram-channel-content-media-scraper-v2").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 = { "channels": "durov, telegram" }

# Run the Actor and wait for it to finish
run = client.actor("webfinity/telegram-channel-content-media-scraper-v2").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 '{
  "channels": "durov, telegram"
}' |
apify call webfinity/telegram-channel-content-media-scraper-v2 --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,webfinity/telegram-channel-content-media-scraper-v2"
        }
    }
}
```

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/f9ah2tzQwzhF8OyfK/builds/eaiUuS3HnA6NKhRzf/openapi.json
