# Telegram Channel Scraper: Posts, Views & Reactions (`reverend_sal/telegram-channel-scraper`) Actor

Scrape posts from public Telegram channels without a login or API key: text, date, views, reactions, photos, videos, files, links, polls, forwards and replies. Date range, keywords and only-new mode. $1 per 1,000 posts.

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

## Pricing

from $1.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 Scraper: posts, views, reactions & media from public channels

Get the posts of any **public Telegram channel** as clean JSON, CSV or Excel: text, date, views, reactions, photos, videos, files, link previews, polls, forwards, replies, hashtags and links. No Telegram account, phone number, API key or login is needed: the Actor reads the same public web preview anyone can open at `https://t.me/s/<channel>`.

You pay **$0.001 per post** delivered ($1 per 1,000). Channels that cannot be read cost nothing.

### Quick start

1. Enter channel usernames or links in **Channels** (one per line).
2. Choose how many posts per channel, and optionally a date range or keywords.
3. Click **Start**. Posts appear in the **Output** tab within seconds; export them as JSON, CSV or Excel, or read them through the API.

### What you can do with it

- **Monitor channels**: run on a schedule with *Only new posts* and get just what was published since the last run (you pay only for new posts).
- **Research and archives**: collect a channel's history, newest first, as far back as you need.
- **Media and news tracking**: filter by date range and keywords across many channels at once.
- **Engagement analysis**: views, reaction counts per emoji, paid star reactions, edit flags.
- **Link and mention extraction**: every link, @mention and #hashtag in a post, plus link-preview titles.

### Input

| Field | What it does | Default |
|---|---|---|
| `channels` | Public channels, one per line: `durov`, `@durov`, `https://t.me/durov`, `https://t.me/s/durov`, a post link like `https://t.me/durov/300`, or `tg://resolve?domain=durov`. Up to 500 per run. | – |
| `maxPostsPerChannel` | Newest posts per channel. `0` = the whole history (bounded by `maxPosts`). | 100 |
| `maxPosts` | Total posts per run across all channels: a hard cap on the cost. | 1000 |
| `postedWithinDays` | Only posts from the last N days (`0` = any date). | 0 |
| `fromDate` / `toDate` | Only posts in this date range (YYYY-MM-DD, UTC, both inclusive). | – |
| `keywords` / `excludeKeywords` | Keep posts containing any keyword; drop posts containing any excluded one. Case-insensitive; searches the post text, link preview, poll and file/audio titles. | – |
| `skipForwarded` | Leave out posts forwarded from elsewhere. | off |
| `onlyNew` + `stateKey` | Deliver only posts not delivered by earlier runs with the same memory key (see below). | off, `default` |
| `includeChannelInfo` | Add channel title, subscriber count and verified flag to every row. | on |
| `includeTextHtml` | Add the post text as HTML (bold, italics, links kept). | off |
| `maxConcurrency` | Channels read in parallel (1-10). Pages of one channel are read in order. | 4 |
| `maxPagesPerChannel` | Safety limit: pages read per channel (about 20 posts each), so a keyword or old date range does not read a huge channel's whole history. `0` = no limit. | 500 |
| `proxyConfiguration` | Optional. Not needed normally; select Apify Proxy if Telegram starts answering "Too Many Requests". | none |

Example:

```json
{
  "channels": ["durov", "https://t.me/telegram", "@tginfo"],
  "maxPostsPerChannel": 50,
  "postedWithinDays": 30,
  "keywords": ["update", "release"]
}
```

### Output

One row per post, newest first within each channel:

```json
{
  "id": "durov/538",
  "channel": "durov",
  "postId": 538,
  "url": "https://t.me/durov/538",
  "date": "2026-09-30T15:04:11.000Z",
  "edited": true,
  "type": "text",
  "text": "Telegram's safety report ...",
  "author": null,
  "views": 4520000,
  "viewsText": "4.52M",
  "reactionsTotal": 76100,
  "reactions": [{ "emoji": "👍", "count": 54600 }, { "emoji": null, "customEmojiId": "5265077361648368841", "count": 9570 }],
  "paidStarReactions": 17900,
  "media": [],
  "linkPreview": { "url": "https://telegram.org/safety", "siteName": "Telegram", "title": "Telegram Safety Overview", "description": "...", "imageUrl": "https://cdn4.telesco.pe/file/..." },
  "poll": null,
  "forwardedFrom": null,
  "replyTo": null,
  "hashtags": [],
  "mentions": [],
  "links": ["https://telegram.org/safety"],
  "channelTitle": "Pavel Durov",
  "channelSubscribers": 10200000,
  "channelVerified": true,
  "scrapedAt": "2026-10-29T11:00:00.000Z"
}
```

- Every post row has the same fields; a value Telegram does not show is `null` (or `0` / `[]` for counts and lists), never missing.
- `type`: `text`, `photo`, `video`, `album`, `audio`, `voice`, `document`, `sticker`, `roundVideo`, `poll`, `location` or `unsupported` (content Telegram does not show on the web; the row still has the date, views and link).
- `media`: one entry per photo, video or file with `type` and, where Telegram shows them, `url` (the photo or video file on Telegram's CDN), `thumbnailUrl`, `duration` (seconds), `title` and `extra` (e.g. the file size or the performer), plus `postUrl`. Videos too large for the web preview have `notAvailableOnWeb: true`; GIFs are videos with `isGif: true`.
- `poll`: `question`, `type` (e.g. "Anonymous Poll"), `voters`, `options` with `percent`.
- `forwardedFrom`: `name`, `channel`, `postId`, `url` of the source when Telegram shows them, and `author`.
- `replyTo`: the post being replied to (`postId`, `url`, `author`, a text snippet).

**Error rows** (free): a channel that cannot be read produces one row with `input`, `errorCode` and `error`:
`INVALID_INPUT`, `PRIVATE_CHANNEL` (invite links, `t.me/c/…`), `NOT_FOUND`, `NOT_A_CHANNEL` (a group, bot or user account), `NO_PUBLIC_PREVIEW` (the channel exists but Telegram does not show it on the web), `FETCH_FAILED`, `RATE_LIMITED`.

The **run summary** (key-value store record `OUTPUT`) lists per channel the **channel profile** (title, description, subscribers, verified flag, photo URL, link), pages read, posts seen, posts matching your filters, posts delivered, how many were filtered out and why, and any error; plus totals and whether a limit or the maximum charge was reached.

### Only new posts (scheduled runs)

Turn on `onlyNew` and schedule the Actor. The first run delivers the newest posts up to your limits and remembers them; each later run with the same `stateKey` delivers only posts published since, and stops reading a channel as soon as it reaches posts it has seen (fast and cheap). If a run stops early (because of `maxPosts` or the maximum charge), the posts it did not reach are delivered by the next run: nothing is lost and nothing is charged twice. Use a different `stateKey` per monitor. The memory is kept in your named key-value store `telegram-channel-scraper-state`.

### Pricing

Pay per result: **$0.001 per post** ($1 per 1,000 posts), plus Apify's standard Actor start fee ($0.00005 per run start). Error rows, service messages ("channel created", "photo updated"), posts filtered out by your settings and posts already delivered in only-new mode are **not charged**. Set a maximum cost per run in the run options: the Actor stops cleanly when it is reached, and you are never charged for a post that was not saved. Platform usage (compute) is included in the price.

What a run costs is the number of posts delivered × $0.001 (+ $0.00005 per start):

| Example run | Posts delivered | Cost |
|---|---|---|
| The default input: 2 channels, 20 newest posts each | 40 | $0.04 |
| 10 channels, last 7 days, a channel posting ~30 times a day | up to ~2,100 | up to ~$2.10 |
| Any run with **Max posts in total** = 500 | at most 500 | at most $0.50 |
| Scheduled **Only new posts** run | only posts published since the last run | $0.001 each; $0.00005 when nothing is new |

### Use it from code, automations or an AI agent

- **One API call, posts back as JSON:** `POST https://api.apify.com/v2/acts/<this Actor>/run-sync-get-dataset-items?token=<YOUR_TOKEN>` with the input as the body (for runs that finish within 5 minutes; see the API tab for the JavaScript, Python and CLI clients).
- **Monitoring feed:** save your input as a task with **Only new posts** on, schedule it, and read `https://api.apify.com/v2/actor-tasks/<task id>/runs/last/dataset/items?status=SUCCEEDED` for the latest new posts, or add a webhook / Slack / Make / Zapier / n8n integration.
- **AI agents (MCP):** the Actor works with the Apify MCP server. The cost of a run is predictable: at most `maxPosts` × $0.001, and unreadable channels are free.

### How it works and limits

- The Actor reads Telegram's public web preview (`t.me/s/<channel>`), about 20 posts per page, newest first, following the "load older" link.
- **Public channels only.** Private channels, invite links, groups, bots and user accounts have no public preview; they are reported as free error rows. Channels Telegram hides from the web (for example for content restrictions) are reported as `NO_PUBLIC_PREVIEW`.
- **View counts are rounded by Telegram** (e.g. "5.85M" becomes 5850000); `viewsText` keeps the exact text shown.
- **Forward sources**: Telegram often hides the original channel of a forwarded post on the web; then `forwardedFrom` has only what is shown (sometimes just the author signature).
- Comments (discussion threads) are not on the public preview and are not included.
- Media URLs point to Telegram's CDN and may expire after a while; download what you need soon after the run.
- Restarts of the run (platform migrations) are handled: posts already delivered are neither delivered nor charged again.

### FAQ

**Do I need a Telegram account or API key?** No. Only public data shown at `t.me/s/<channel>` is read.

**Can it read groups or private channels?** No. Only public channels have a web preview.

**How far back can it go?** As far as the channel's public history goes; set `maxPostsPerChannel` to `0` and use `maxPosts` or a date range to bound the run.

**My run returned 0 posts. Why?** The run's status message says why: your filters removed every post (it lists which filter removed how many), the channel has no posts in the range read, or, in only-new mode, nothing was published since the last run.

**Can I get the channel's profile (description, photo, subscribers)?** Yes: every row has the title, subscribers and verified flag, and the run summary (`OUTPUT`) has the full profile per channel, at no charge.

**Does it download photos and videos?** No. Rows contain the media URLs on Telegram's CDN (they may expire), so download what you need soon after the run.

**Do I need a proxy?** Usually not. If a run reports `RATE_LIMITED`, enable Apify Proxy in the input; blocked sessions are replaced automatically.

**Is it legal?** The Actor reads only information that Telegram publishes openly on the web. You are responsible for how you use the data, including personal data rules (such as the GDPR) that apply to you.

# Actor input Schema

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

One public channel per line: a username ("durov" or "@durov") or a link (https://t.me/durov, https://t.me/s/durov, or a post link like https://t.me/durov/300). Private channels and invite links (t.me/+…) cannot be read. Groups, bots and user accounts are reported as free error rows.

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

The newest posts first, up to this many per channel. 0 = all the channel's posts (bounded by "Max posts in total").

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

Stop after this many posts across all channels. You pay per post delivered, so this caps the cost of a run (as does the run's max charge).

## `postedWithinDays` (type: `integer`):

Only posts from the last N days. 0 = any date. Reading stops at the first older post, so this also makes runs faster.

## `fromDate` (type: `string`):

Only posts on or after this date (YYYY-MM-DD, UTC).

## `toDate` (type: `string`):

Only posts on or before this date (YYYY-MM-DD, UTC). Newer posts are read but not delivered or charged.

## `keywords` (type: `array`):

Only posts containing at least one of these words or phrases (case-insensitive; searches the text, link preview, poll and file names).

## `excludeKeywords` (type: `array`):

Leave out posts containing any of these words or phrases.

## `skipForwarded` (type: `boolean`):

Leave out posts the channel forwarded from elsewhere.

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

Remember the posts delivered and, on later runs with the same memory key, deliver only posts published since (you pay only for new ones). Ideal for scheduled monitoring. The first run delivers the newest posts up to the limits above.

## `stateKey` (type: `string`):

Name of the memory used by "Only new posts". Runs with the same key share it; use different keys for separate monitors. Stored in your named key-value store "telegram-channel-scraper-state".

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

Adds channelTitle, channelSubscribers and channelVerified to every post row.

## `includeTextHtml` (type: `boolean`):

Adds textHtml: the post text as HTML with bold, italics and links kept.

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

How many channels are read at the same time (pages of one channel are always read in order).

## `maxPagesPerChannel` (type: `integer`):

Safety limit on reading: at most this many pages (about 20 posts each) per channel. Matters when filters (keywords, an old date range) skip most posts, so the Actor would otherwise read a big channel's whole history. 500 pages ≈ 10,000 posts. 0 = no limit.

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

Not needed in most cases: t.me pages are public. If Telegram starts refusing requests (HTTP 429), select Apify Proxy here; a blocked session is replaced automatically.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "telegram"
  ],
  "maxPostsPerChannel": 20,
  "maxPosts": 1000,
  "postedWithinDays": 0,
  "fromDate": "",
  "toDate": "",
  "keywords": [],
  "excludeKeywords": [],
  "skipForwarded": false,
  "onlyNew": false,
  "stateKey": "default",
  "includeChannelInfo": true,
  "includeTextHtml": false,
  "maxConcurrency": 4,
  "maxPagesPerChannel": 500,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Channel posts with date, text, views, reactions, media, link preview, poll, forward and reply details.

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

Per channel: title, subscribers, pages read, posts seen, posts matching the filters, posts delivered, errors.

# 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": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("reverend_sal/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": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("reverend_sal/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": 20
}' |
apify call reverend_sal/telegram-channel-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,reverend_sal/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/1bFXowgyekPddaRSV/builds/eKKZC1BfDlLQmrsdb/openapi.json
