# Telegram Channel Scraper: Public Posts, Views & Media (`subimpact/telegram-channel-scraper`) Actor

Scrape public Telegram channels via the t.me web preview: one row per post with text, date, views, media, link previews, polls, reactions, hashtags and links, plus channel info. No login, no API key, no browser. $0.30 per 1,000 posts.

- **URL**: https://apify.com/subimpact/telegram-channel-scraper.md
- **Developed by:** [subimpact](https://apify.com/subimpact) (community)
- **Categories:** Social media, News, Lead generation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 posts

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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 Scraper: Public Posts, Views & Media

Read public Telegram channels through their `t.me/s/<channel>` web preview and get clean rows back: post text,
publication time, view counts, media type, photo and video thumbnail URLs, document names, link previews, polls,
reactions, hashtags, mentions and outgoing links. One channel-info row per channel (title, description, subscribers,
media counters, avatar, verified flag), then one row per post. No login, no API key, no browser, no Telegram app.

Pay only for what you get: **$0.30 per 1,000 posts** and **$1.00 per 1,000 channel-info rows**. A default run over one
channel (50 posts plus its info row) costs about **$0.016**, plus Apify platform usage.

### What you get

- **One row per post**, newest first: `postId`, `postUrl`, `postedAt`, `text` (line breaks kept, HTML stripped),
  `views` as an integer plus the `viewsText` Telegram printed ("5.84M" becomes `5840000`).
- **Media without downloading it**: `mediaType` (photo, video, document, audio, voice, sticker, poll, mixed, none),
  `photoUrls`, `videoThumbUrls`, `videoDurationSec`, `documentNames`.
- **Rich post details**: `linkPreview`, `poll` with option percentages, `reactions`, `hashtags`, `mentions`, `links`,
  `replyToPostId`, `isForwarded` plus `forwardedFrom`, `authorSignature`, `isEdited`.
- **Channel info row**: `description`, `subscribers`, `photosCount`, `videosCount`, `linksCount`, `filesCount`,
  `avatarUrl`, `isVerified`, `channelUrl`.
- **Filters that save money**: `maxPostsPerChannel`, `maxTotalPosts`, `postedAfter`, `postedBefore`, and Telegram's own
  in-channel search via `searchQuery`.
- **Delta mode (`onlyNew`)** for schedules: each run delivers only posts newer than the highest post id already
  delivered for that channel. The cursor lives in a key-value store in your own Apify account.
- **Single post URLs**: give `https://t.me/durov/531` and you get that post only.

Every run also writes a `RUN_SUMMARY` record to the run's key-value store: per-channel status, rows, requests and any
problem, so a schedule can be checked without reading logs.

### Quick start

Input that works exactly as written:

```json
{
  "channels": ["durov", "telegram"],
  "maxPostsPerChannel": 50,
  "includeChannelInfo": true
}
```

Channel entries may be written as `durov`, `@durov`, `t.me/durov`, `https://t.me/durov`, `https://t.me/s/durov`, or a
single post such as `https://t.me/durov/531`.

A monitoring run for one channel, new posts only:

```json
{
  "channels": ["telegram"],
  "onlyNew": true,
  "includeChannelInfo": false,
  "maxPostsPerChannel": 100
}
```

Search inside a channel and keep one week of posts:

```json
{
  "channels": ["durov"],
  "searchQuery": "Telegram",
  "postedAfter": "2026-10-18",
  "includeChannelInfo": false
}
```

### Output example

A post row from `t.me/s/durov` (CDN image URLs shortened here for readability):

```json
{
  "recordType": "post",
  "channel": "durov",
  "channelTitle": "Pavel Durov",
  "postId": 531,
  "postUrl": "https://t.me/durov/531",
  "postedAt": "2026-07-16T20:45:25.000Z",
  "isEdited": false,
  "editedAt": null,
  "text": "🩵 Telegram now has a Rich Text Editor.\n\nIt supports tables, headings, inline media, lists, AI content, formulas, and more.\n\n😈 Messaging apps will never be the same!",
  "views": 5840000,
  "viewsText": "5.84M",
  "authorSignature": "Pavel Durov",
  "isForwarded": false,
  "forwardedFrom": null,
  "replyToPostId": null,
  "mediaType": "video",
  "photoUrls": [],
  "videoThumbUrls": ["https://cdn4.telesco.pe/file/JcEMS9Bq90aZQ4T4hTjXv8G...jpg"],
  "videoDurationSec": null,
  "documentNames": [],
  "linkPreview": null,
  "poll": null,
  "reactions": [
    { "emoji": "stars", "count": 6300 },
    { "emoji": "emoji-id:5265077361648368841", "count": 54600 }
  ],
  "hashtags": [],
  "mentions": [],
  "links": [],
  "scrapedAt": "2026-10-25T07:49:34.378Z"
}
```

The matching channel row:

```json
{
  "recordType": "channel",
  "channel": "durov",
  "channelTitle": "Pavel Durov",
  "description": "Founder of Telegram.",
  "subscribers": 10300000,
  "photosCount": 102,
  "videosCount": 47,
  "linksCount": 200,
  "filesCount": null,
  "avatarUrl": "https://cdn4.telesco.pe/file/enJjZA2XzVbFAjeTOqFmqCmKVB5C...jpg",
  "isVerified": true,
  "channelUrl": "https://t.me/durov",
  "scrapedAt": "2026-10-25T07:49:34.378Z"
}
```

A poll row reads like this (`mediaType` is `poll`, question and percentages as Telegram shows them):

```json
{
  "recordType": "post",
  "channel": "polldemo",
  "postId": 101,
  "postedAt": "2026-10-20T09:15:00.000Z",
  "views": 18400,
  "mediaType": "poll",
  "poll": {
    "question": "Which Telegram app do you use most?",
    "options": [
      { "text": "Mobile", "percent": 52 },
      { "text": "Desktop", "percent": 31 },
      { "text": "Web", "percent": 17 }
    ]
  }
}
```

### Pricing

Pay per event, charged only after the row is in the dataset:

| Event | Price | Charged when |
|---|---|---|
| `post` | $0.0003 per post ($0.30 per 1,000) | a post row was pushed |
| `channel-info` | $0.001 per channel ($1.00 per 1,000) | a channel-info row was pushed |
| `apify-actor-start` | $0.00005 per run | the run starts (platform event) |

Buyer pays Apify platform usage separately. Empty channels, search results with no matches, preview-disabled channels
and failed requests cost nothing: no row, no charge. Turn `includeChannelInfo` off and you pay only for posts.

Typical costs: 500 posts from one channel with channel info = 500 x $0.0003 + $0.001 = **$0.151**. A scheduled hourly
`onlyNew` run that finds 3 posts = **$0.0009**.

### How this compares

Free-plan list prices on the Apify Store, checked 25 October 2026. Competitor platform usage is charged on top by
their listings; check the individual listing, prices change.

| Actor | Start fee | Per 1,000 messages | Per 1,000 channel infos | Users on the listing |
|---|---|---|---|---|
| **telegram-channel-scraper** (this actor) | $0.00005 platform start | **$0.30** | **$1.00** | new |
| automation-lab/telegram-scraper | $0.005 | $1.00 | $2.00 | 189 in 30 days |
| khadinakbar/telegram-channel-scraper | $0.003 | $2.00 | not listed | 61 |
| tugelbay/telegram-posts-scraper | none listed | $2.00 | not listed | 27 |

What is different here beyond the price:

- **Rows as they are ready**: every post row is pushed and charged the moment it is parsed, so a run that hits its
  time limit still delivers everything it collected.
- **Caps that hold**: `maxPostsPerChannel` plus a global `maxTotalPosts` stop a run before it surprises you.
- **Delta mode built in**: `onlyNew` keeps a per-channel post-id cursor in your own key-value store, and never moves the
  cursor backwards after a failed page.
- **Per-channel status record**: `RUN_SUMMARY` says which channel was ok, empty, not found, or had its web preview
  disabled, and how many requests it took.

### Limits and honest notes

- **Public channels with the web preview enabled only.** Private groups, private channels (`t.me/c/...`), invite links
  (`t.me/+...`) and channels whose owner disabled the preview cannot be read. They come back as
  `preview_unavailable` or `not_found` in `RUN_SUMMARY`, with no row and no charge. Telegram groups show no web
  preview at all.
- **Telegram serves 20 posts per page.** The actor pages with `?before=<post id>` and pauses 300 to 800 ms between
  pages of the same channel. `maxPostsPerChannel` is a cap, not a promise: a channel with fewer posts returns fewer.
- **Counts are a snapshot.** `views`, `subscribers`, `reactions` and poll percentages are what Telegram printed at
  scrape time.
- **`editedAt` is always null**: Telegram marks a post as edited (`isEdited: true`) but never prints the edit time on
  the public preview.
- **`forwardedFrom` can be null even when `isForwarded` is true**: Telegram hides the source name for forwards from
  private or hidden sources.
- **Media are links, not files.** Photo URLs and video thumbnails are the CDN links from the page, and Telegram's CDN
  links can expire. Nothing is downloaded.
- **Animated GIFs** are rendered by Telegram's preview as muted video loops, so they arrive as `mediaType: "video"`.
- **In-channel search is Telegram's own**: it matches post text, results are ordered oldest to newest, and paging uses
  the same `before=` parameter. It is case-insensitive but not a full-text engine over the whole archive. A single post
  URL returns that post, `searchQuery` applies to channel timelines only.
- **Reaction glyphs**: Telegram prints custom emoji as an image with an id, so such reactions carry
  `emoji: "emoji-id:<id>"`; paid star reactions are reported as `"stars"`.
- **One post-id watermark per channel** for `onlyNew`. If you also use `searchQuery` with `onlyNew`, the cursor is
  shared with the timeline run of that channel; use a separate `stateStoreName` to keep them apart.
- **Direct requests by default.** Ten parallel direct requests to `t.me` answered fine when this actor was built. Enable
  Apify proxy if your runs get rate limited; a proxy failure fails the request, it never falls back to a direct one.

### Responsible use

This actor reads the same pages any browser sees on `t.me`, for public channels only. Only collect channels you have a
reason to collect. Post text can contain personal data: how you store, process and share it is your responsibility
under the GDPR, PDPA and any other law that applies to you. Do not use it to build profiles of private individuals or
to evade Telegram's rate limits.

### Development

```bash
npm ci
npm test          # node:test, parses the saved t.me fixtures
npm run build
```

Fixtures in `test/fixtures/` were saved from live `t.me` pages on 25 October 2026 (tokens and CDN file names
scrubbed). `docs/PARSING.md` lists the selectors the parser relies on.

# Actor input Schema

## `channels` (type: `array`):

Public channels to read. Any of these forms works: durov, @durov, t.me/durov, t.me/s/durov, https://t.me/durov, or a single post URL such as https://t.me/durov/531 (that post only). Private groups, invite links (t.me/+hash) and channels with the web preview disabled are not readable and are reported in the run summary.

## `maxPostsPerChannel` (type: `integer`):

How many posts to collect from each channel, newest first (1 to 10000). Telegram serves 20 posts per page, so this also decides how many pages are fetched.

## `maxTotalPosts` (type: `integer`):

Hard cap on post rows across all channels in one run (1 to 1000000). The run stops cleanly when it is reached.

## `searchQuery` (type: `string`):

Only posts matching this text, using Telegram's own in-channel search (t.me/s/<channel>?q=...). Works with paging and with every other option. Leave empty to read the channel timeline.

## `postedAfter` (type: `string`):

ISO date or date-time. Posts older than this are skipped, and paging stops as soon as a page reaches older posts. Example: 2026-08-01 or 2026-08-01T09:00:00Z.

## `postedBefore` (type: `string`):

ISO date or date-time. Newer posts are skipped (paging still starts from the newest page). Example: 2026-10-01.

## `includeChannelInfo` (type: `boolean`):

Push a channel row (title, description, subscriber count, media counters, avatar, verified flag) before that channel's posts. Costs one channel-info event per channel.

## `onlyNew` (type: `boolean`):

Deliver only posts newer than the highest post id already delivered for that channel. The cursor is kept in the named key-value store below, so scheduled runs return just the new posts.

## `stateStoreName` (type: `string`):

Name of the key-value store in your Apify account that keeps the per-channel post-id cursors. Leave the default unless you run several configurations of this actor and want separate cursors.

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

How many channels are fetched at the same time (1 to 10). Pages inside a channel are always fetched one after another with a short delay.

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

Direct requests are the default: t.me answered 10 of 10 parallel direct requests when this actor was built. Enable Apify proxy only if your runs are rate limited. A proxy failure fails the request; it never falls back to a direct request.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "@telegram",
    "https://t.me/s/durov",
    "https://t.me/durov/531"
  ],
  "maxPostsPerChannel": 50,
  "maxTotalPosts": 500,
  "searchQuery": "Telegram",
  "postedAfter": "2026-08-01",
  "postedBefore": "2026-10-01",
  "includeChannelInfo": true,
  "onlyNew": false,
  "stateStoreName": "telegram-channel-scraper-state",
  "maxConcurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `recordType` (type: `string`):

post for a message row, channel for a channel-info row

## `channel` (type: `string`):

Channel username without the @, e.g. durov

## `channelTitle` (type: `string`):

Channel display name, e.g. Pavel Durov

## `postId` (type: `string`):

Telegram message id inside the channel (post rows only)

## `postUrl` (type: `string`):

Canonical public link, https://t.me/<channel>/<postId> (post rows only)

## `postedAt` (type: `string`):

Publication time from the post's <time datetime> attribute, ISO 8601 UTC (post rows only)

## `isEdited` (type: `string`):

True when Telegram marks the post as edited (post rows only)

## `editedAt` (type: `string`):

Edit timestamp. The t.me web preview does not publish one, so this stays null; use isEdited to filter edited posts (post rows only)

## `text` (type: `string`):

Plain-text post body, line breaks kept, emoji kept, HTML stripped (post rows only)

## `views` (type: `string`):

View count as an integer: 5.84M becomes 5840000 (post rows only)

## `viewsText` (type: `string`):

View count exactly as Telegram printed it, e.g. 5.84M (post rows only)

## `authorSignature` (type: `string`):

Signature shown under the post when the channel signs messages, e.g. Pavel Durov (post rows only)

## `isForwarded` (type: `string`):

True when the post is a forward (post rows only)

## `forwardedFrom` (type: `string`):

Source of a forwarded post: {name, url}. Null when not forwarded or when Telegram hides the source name (post rows only)

## `replyToPostId` (type: `string`):

Post id this message replies to, or null (post rows only)

## `mediaType` (type: `string`):

photo, video, document, audio, voice, sticker, poll, gif, mixed or none (post rows only)

## `photoUrls` (type: `string`):

Photo URLs taken from the message's background-image styles (post rows only)

## `videoThumbUrls` (type: `string`):

Video poster image URLs (post rows only)

## `videoDurationSec` (type: `string`):

Video length in seconds when Telegram prints it, e.g. 0:08 becomes 8 (post rows only)

## `documentNames` (type: `string`):

File names of attached documents or audio tracks (post rows only)

## `linkPreview` (type: `string`):

Attached link preview: {url, siteName, title, description, imageUrl} (post rows only)

## `poll` (type: `string`):

Poll question and options with percentages: {question, options:\[{text, percent}]} (post rows only)

## `reactions` (type: `string`):

Reactions as \[{emoji, count}], emoji as the glyph when Telegram prints it, otherwise the emoji id or stars for paid reactions (post rows only)

## `hashtags` (type: `string`):

Hashtags found in the post text, without the # (post rows only)

## `mentions` (type: `string`):

Usernames mentioned in the post text, without the @ (post rows only)

## `links` (type: `string`):

Outgoing http(s) links found in the post text (not hashtags, mentions or the post's own link) (post rows only)

## `description` (type: `string`):

Channel bio shown in the web preview (channel rows only)

## `subscribers` (type: `string`):

Subscriber count as an integer: 10.3M becomes 10300000 (channel rows only)

## `photosCount` (type: `string`):

Photo counter from the channel page (channel rows only)

## `videosCount` (type: `string`):

Video counter from the channel page (channel rows only)

## `linksCount` (type: `string`):

Link counter from the channel page (channel rows only)

## `filesCount` (type: `string`):

File counter from the channel page when Telegram shows one (channel rows only)

## `avatarUrl` (type: `string`):

Channel avatar image URL (channel rows only)

## `isVerified` (type: `string`):

True when the channel page carries Telegram's verified badge (channel rows only)

## `channelUrl` (type: `string`):

https://t.me/<channel> (channel rows only)

## `scrapedAt` (type: `string`):

When this row was scraped, ISO 8601 UTC

# 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"
    ],
    "maxPostsPerChannel": 50,
    "maxTotalPosts": 500,
    "includeChannelInfo": true,
    "onlyNew": false,
    "stateStoreName": "telegram-channel-scraper-state",
    "maxConcurrency": 3,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("subimpact/telegram-channel-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 = {
    "channels": [
        "durov",
        "telegram",
    ],
    "maxPostsPerChannel": 50,
    "maxTotalPosts": 500,
    "includeChannelInfo": True,
    "onlyNew": False,
    "stateStoreName": "telegram-channel-scraper-state",
    "maxConcurrency": 3,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("subimpact/telegram-channel-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 '{
  "channels": [
    "durov",
    "telegram"
  ],
  "maxPostsPerChannel": 50,
  "maxTotalPosts": 500,
  "includeChannelInfo": true,
  "onlyNew": false,
  "stateStoreName": "telegram-channel-scraper-state",
  "maxConcurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call subimpact/telegram-channel-scraper --silent --output-dataset

```

## MCP server setup

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