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

Reads public Telegram channels over plain HTTP -- no login, no session string, no bot token. Returns each post's text, date, views, reactions, photos, videos, polls, replies and forwards, plus the channel profile. Tells a private or non-existent handle apart from an empty channel.

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

## Pricing

from $2.10 / 1,000 results

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

## Telegram Channel Scraper — Posts, Views & Reactions

Reads **public Telegram channels over plain HTTP**. No login, no session
string, no bot token, no API id/hash, no phone number.

Most Telegram scrapers drive MTProto with a logged-in user account. This one
reads `https://t.me/s/<channel>` — the server-rendered preview Telegram itself
publishes to anonymous visitors — so there is no account to get rate-limited,
flagged or banned, and nothing to hand over.

### What you get

Three record types share one dataset, told apart by `recordType`:

| `recordType` | One per | Carries |
| --- | --- | --- |
| `CHANNEL` | channel | title, `@username`, description, verified flag, subscriber count, photo/video/link counters, avatar URL, and the channel's **internal numeric id** |
| `MESSAGE` | post | id, permalink, ISO date + unix timestamp, full text (and its HTML), views, reactions, photos, videos (with direct CDN URLs), polls, link previews, reply quotes, forward source, inline buttons, author signature |
| `SEARCH_SUMMARY` | channel | what was actually applied, pages fetched, id range covered, duplicates skipped, whether the walk hit your cap or the start of the channel |
| `ERROR` | failed input | a named reason — every input maps to at least one row |

### Input

| Field | Meaning |
| --- | --- |
| `channels` | `@durov`, `durov`, `t.me/durov` or `https://t.me/s/durov` — all accepted |
| `searchQuery` | optional keyword filter, applied by Telegram inside each channel |
| `maxMessagesPerChannel` | `0` walks the whole readable history back to post #1 |
| `sinceMessageId` | incremental mode: only posts newer than an id you already have |
| `startFromMessageId` | resume a deep backward crawl from a known id |
| `includeMessages` / `includeChannelProfile` | either half can be switched off |

For scheduled monitoring: take `lastMessageId` from the summary row and feed
it back as `sinceMessageId` next run.

### What it will tell you that a naive scraper will not

**A handle that is not a readable channel still returns HTTP 200.** Telegram
redirects `/s/<handle>` to `/<handle>` and serves a normal-looking landing
page with zero messages. Four different situations look identical at the
status-code level, and this actor names each one:

- `channel_not_found` — the handle does not exist
- `not_a_channel` — it is a user account, or a bot
- `preview_unavailable` — a **real** channel that publishes no public preview
  (private, a group, or "Preview channel" switched off)

That third case is the one that quietly costs you data. `@cryptocom` is a
genuine channel with 366 subscribers whose history is not publicly readable —
a scraper that only counts messages reports "0 posts" and the run looks
successful.

**Counts are rounded by Telegram before they reach the page.** Views arrive as
`"447K"`, subscribers as `"9.72M"`. Both forms are kept: `viewsRaw` /
`subscribersRaw` hold Telegram's own string, `views` / `subscribers` hold the
expanded integer, and `viewsApproximate` / `subscribersApproximate` tell you
when precision was already lost upstream. The exact number is not in the
document at all.

**Custom-emoji reactions carry no emoji.** Premium reactions render as an
empty `<tg-emoji emoji-id="…">` element — the character itself is absent from
the HTML. Rows carry `emoji` (null for those) *and* `customEmojiId`, plus an
`isPaid` flag for Telegram Stars reactions.

### Known limits — Telegram's, not this actor's

- **File attachments are not rendered in the public preview.** Documents,
  voice notes, audio tracks, stickers and round videos have no markup on the
  page at all. A post on a file-sharing channel arrives with its caption text
  and no file. Verified against several such channels; `SEARCH_SUMMARY`
  states this on every run as `previewRendersFileAttachments: false`.
- **Poll results are percentages.** Per-option vote counts are not published;
  only the percentage per option and the total voter count. No absolute
  per-option number is invented from them.
- **Comment threads are not part of the preview** — discussion-group replies
  live in a linked group, which has no public preview of its own.
- Private channels and invite links (`t.me/+…`, `t.me/joinchat/…`) are out of
  scope by construction: there is nothing public to read.
- Video URLs are direct, tokenised Telegram CDN links. They work, and the
  token expires — download promptly rather than storing the URL.

### Politeness

One origin serves every public channel, so `minRequestInterval` defaults to
1 second and is exposed as an input. `t.me/robots.txt` returns HTTP 404, which
RFC 9309 §2.3.1.3 treats as no restrictions; no WAF was observed (7 of 7 TLS
profiles clean on a cold request). A proxy is optional and off by default.

# Actor input Schema

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

Public Telegram channels to scrape. Accepts '@durov', 'durov', 't.me/durov' or 'https://t.me/s/durov'. Private invite links (t.me/+... or t.me/joinchat/...) are not public channels and have no readable preview.

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

Optional keyword filter applied by Telegram itself inside each channel. Verified honest: a term with no matches returns an empty history rather than the unfiltered feed. Leave empty to walk the channel in date order.

## `includeMessages` (type: `boolean`):

Fetch the posts themselves, not just the channel profile. 20 messages per request.

## `includeChannelProfile` (type: `boolean`):

Emit one CHANNEL row per channel with its title, description, subscriber count, avatar and internal numeric channel id.

## `maxMessagesPerChannel` (type: `integer`):

Stop after this many messages per channel. Set 0 to walk the whole readable history back to the first post.

## `sinceMessageId` (type: `integer`):

Incremental mode: walk FORWARD from a message id you already have, oldest first. Use the summary row's lastMessageId from your previous run. Cannot be combined with 'Start from message id'.

## `startFromMessageId` (type: `integer`):

Begin the backward walk just before this message id instead of at the newest post. Useful for resuming a deep history crawl. Cannot be combined with 'Only messages newer than this id'.

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

Page fetches in flight at once across all channels.

## `minRequestInterval` (type: `integer`):

Politeness pacing shared across all workers. One origin serves every public channel, so this is left above zero by default.

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

Optional and OFF by default. No WAF was observed on t.me (7 of 7 TLS profiles clean). Worth switching on for large runs.

## Actor input object example

```json
{
  "channels": [
    "@telegram",
    "durov"
  ],
  "searchQuery": "airdrop",
  "includeMessages": true,
  "includeChannelProfile": true,
  "maxMessagesPerChannel": 100,
  "maxConcurrency": 3,
  "minRequestInterval": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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": [
        "@telegram"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/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": ["@telegram"] }

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

```

## MCP server setup

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