# Telegram Channel Scraper (`bigibyte/telegram-channel-scraper`) Actor

Telegram channel scraper that exports public posts with views, reactions, and media, plus subscriber counts, so you can monitor t.me channels without a login or a bot token.

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

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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: Export Public Posts, Views, and Reactions

This Telegram scraper reads public channels from the web preview at `t.me/s/{channel}` and saves posts, views, reactions, and channel details. You can download JSON, CSV, or Excel, or send the dataset to Google Sheets, Make, Zapier, or n8n. There is no login, no bot token, and no Telegram API key.

It only sees what Telegram shows on the public preview. Private channels, invite links, and channels that have turned the preview off (BBC News and Reuters, for example) do not return posts.

### Use cases

- News and crypto monitoring: keep the text, view count, and reactions from a list of public channels.
- Brand mentions: search inside a channel with `searchQuery`, then page back through older hits.
- OSINT and research: export forwards, reply links, and dates without opening Telegram.
- Competitor channels: store subscriber counts from the public landing page and rerun them on a schedule.
- Content and AI datasets: save post text as plain text, Markdown, or HTML, with the photo and video URLs Telegram embeds.

### How to run it

1. Choose posts, a single post URL, or channel info.
2. Paste usernames (`durov`, `@durov`), channel links (`https://t.me/durov`, `https://t.me/s/durov`), or post links (`https://t.me/durov/400`).
3. Set a max result count. The free cap is 2,000 rows per run.
4. Start the run and export the dataset.

A bad username does not stop the run. The actor writes an error row and moves on.

### Input

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `mode` | string | `posts` | `posts`, `post_details`, or `channel_info`. |
| `channels` | string\[] | `durov`, `telegram` | Usernames, `t.me` links, or post URLs. |
| `searchQuery` | string | empty | Optional keyword. Telegram's preview search (`?q=`). |
| `dateFrom` | string | empty | `YYYY-MM-DD`. Paging stops once posts are older than this date. |
| `dateTo` | string | empty | `YYYY-MM-DD`. Newer posts are skipped. |
| `includeChannelInfo` | boolean | `false` | Add the channel object onto each post. |
| `includeServiceMessages` | boolean | `false` | Keep service messages such as "channel created". |
| `textFormat` | string | `text` | `text`, `markdown`, or `html`. |
| `maxItems` | integer | `100` | Cap for the whole run. Maximum 2000. |
| `maxPostsPerChannel` | integer | `100` | Cap for each channel. |
| `maxConcurrency` | integer | `5` | Simultaneous requests. Maximum 5. |
| `timeoutSecs` | integer | `300` | Stop after this many seconds and keep rows already saved. |
| `requestTimeoutSecs` | integer | `30` | Per-request timeout. |
| `proxyConfiguration` | object | No proxy | Optional Apify proxy. Leave it off unless you hit 429 or 403. |

#### Posts

```json
{
    "mode": "posts",
    "channels": ["durov", "https://t.me/s/telegram"],
    "maxItems": 100,
    "maxPostsPerChannel": 40
}
```

#### One post

```json
{
    "mode": "post_details",
    "channels": ["https://t.me/durov/400"],
    "textFormat": "markdown"
}
```

#### Channel info

```json
{
    "mode": "channel_info",
    "channels": ["durov", "@telegram", "https://t.me/cnnbrk"]
}
```

### What data does this actor extract?

Every row has `mode`, `scrapedAt`, and `sourceInput`. Failures use `error` and `statusCode`. Codes: `PREVIEW_DISABLED`, `NOT_FOUND`, `PRIVATE_NOT_SUPPORTED`, `HTTP_<code>`, `PARSE_ERROR`, `INVALID_INPUT`.

The channel table uses `channelTitle` and `channelDescription` because Apify's dataset schema rejects the field names `title` and `description`. The JSON row still includes `title` and `description` with the same values. Link previews keep their own `title` and `description` inside `linkPreview`.

View and reaction numbers are parsed from labels such as `5.79M` and `54.6K`. Those parsed values are rounded. `viewsRaw`, `countRaw`, and `subscribersRaw` keep the original text.

#### Posts

```json
{
    "mode": "posts",
    "postId": 531,
    "channel": "durov",
    "url": "https://t.me/durov/531",
    "date": "2026-07-16T20:45:25+00:00",
    "text": "Telegram now has a Rich Text Editor.",
    "views": 5790000,
    "viewsRaw": "5.79M",
    "authorSignature": "Pavel Durov",
    "isForwarded": false,
    "photos": [],
    "videos": [{"thumbnailUrl": "https://cdn4.telesco.pe/file/example", "videoUrl": "https://cdn4.telesco.pe/file/example.mp4?token=..."}],
    "reactions": [{"emoji": null, "emojiId": "5265077361648368841", "count": 54600, "countRaw": "54.6K", "isPaid": false}],
    "isService": false,
    "sourceInput": "durov"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `postId`, `channel`, `url`, `date` | integer, string, string, string | Message id, username, permalink, and the `datetime` Telegram prints. |
| `text`, `textHtml` | string | Text in the format you chose, plus the HTML. Custom emoji keep the inner character. |
| `views`, `viewsRaw` | integer, string | Parsed views and the original label. |
| `authorSignature` | string | Signed author, when the post has one. |
| `isForwarded`, `forwardedFromName`, `forwardedFromUrl` | boolean, string, string | Forward label and the source message link. |
| `replyToUrl` | string | The message this post replies to. |
| `links`, `photos` | string\[] | Links in the text, and photo URLs from `background-image`. |
| `videos` | object\[] | `thumbnailUrl` and `videoUrl`. The file URL is a temporary token. It expires. |
| `isAlbum` | boolean | Grouped photo or video post. |
| `linkPreview` | object | `url`, `siteName`, `title`, `description`, `imageUrl`. |
| `reactions`, `totalReactions` | object\[], integer | Emoji or custom emoji id, parsed count, raw count, and whether it is a paid star reaction. |
| `isService` | boolean | Service message. Skipped unless you opt in. |
| `channelInfo` | object | Present when `includeChannelInfo` is on. |

#### Channels

```json
{
    "mode": "channel_info",
    "channel": "durov",
    "url": "https://t.me/durov",
    "title": "Pavel Durov",
    "channelTitle": "Pavel Durov",
    "description": "Founder of Telegram.",
    "isVerified": true,
    "subscribers": 10428980,
    "subscribersRaw": "10 428 980 subscribers",
    "photosCount": 102,
    "videosCount": 47,
    "linksCount": 200,
    "previewEnabled": true
}
```

| Field | Type | Description |
|-------|------|-------------|
| `channel`, `url`, `title`, `channelTitle` | string | Username, public URL, and display name. |
| `description`, `channelDescription` | string | The public description. |
| `photoUrl`, `isVerified` | string, boolean | Photo and the verified badge. |
| `subscribers`, `subscribersRaw` | integer, string | Exact count from the landing page when Telegram prints one, otherwise the compact counter from the preview. |
| `photosCount`, `videosCount`, `filesCount`, `linksCount` | integer | Counters from the preview header. Empty when the preview is off. |
| `previewEnabled` | boolean | True when `t.me/s/{channel}` returns posts. |
| `lastPostId`, `lastPostDate` | integer, string | Newest post on the first preview page. |

### Limits and notes

- 2,000 results per run. `maxItems` stops the run once that many rows are stored. Duplicate post ids are skipped.
- Each preview page has about 20 posts. The actor follows `rel="prev"` (`?before=`) toward older posts.
- Channels with the web preview disabled, such as `bbcnews` and `reuters`, return `PREVIEW_DISABLED` in posts mode. Channel info still uses the public landing page, which may have a title and subscriber count or almost nothing.
- A username that redirects to `telegram.org`, such as `wsj` when this was checked, returns `NOT_FOUND`.
- Invite links (`t.me/+...`, `joinchat`) return `PRIVATE_NOT_SUPPORTED`.
- Video file URLs include a token and expire. Save the file during the run if you need the bytes later.
- Subscriber figures are whatever Telegram prints in public. A landing page count like `10 429 228 subscribers` is exact. A header label like `10.4M` is already rounded by Telegram, and parsing it keeps that rounding.
- This is not an official Telegram product. Use it within Telegram's terms. It does not log in and it does not join private chats.

### FAQ

**Does this use the Telegram API?**

No. It downloads the public web preview at `https://t.me/s/{channel}` and the channel landing page at `https://t.me/{channel}`. You do not need an API id, a bot token, or a logged-in session.

**How far back can a Telegram channel scraper go?**

It pages with `?before={postId}` until Telegram stops offering an older page, the post id reaches 1, your date filter hits, or the result cap is full. Channels with a long history can take many requests. `maxPostsPerChannel` is there for that.

**Can it scrape groups or private channels?**

No. Public channels with the web preview on are the supported case. Private invites are rejected. If the preview is off, post text is not on the public page.

**Why is the view count not exact?**

Telegram prints compact labels such as `5.79M` on the preview. The actor stores that string in `viewsRaw` and a parsed integer in `views`. `5.79M` becomes 5790000, which is the label times one million, not a secret exact counter.

**Do the photo and video URLs keep working?**

Photo URLs from the preview are plain CDN links. Video `src` URLs carry a token and expire, so a later download can 404 even though the post row is fine.

### Related tools

A Substack newsletter scraper is the usual pair if you are tracking writers who post in both places. For a large channel list, alerts, or a pipeline that stores media before the video token expires, that is a different job than this actor.

Need monitoring of hundreds of channels, alerts, or a custom pipeline? BigiByte builds custom scrapers and APIs: https://bigibyte.com

### Trademark Notice

Telegram and related names, logos, and trademarks belong to their respective owners. Their use in this documentation is solely to identify the source of the data and does not imply affiliation, sponsorship, endorsement, or partnership.

### Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, sponsored by, or otherwise officially associated with Telegram.

### Is web scraping legal?

Collecting information that is already public is generally legal. Scraping is not the same thing as hacking, and there is no single law that bans it. The limits are the usual ones: do not break into accounts, do not take private or confidential data, and be careful with personal data and copyrighted works. A site's terms can also restrict how you access it. Facts such as a public name, price, or rating are not the same thing as republishing someone's article or photo.

This actor reads public pages or official public APIs. It does not log in and it does not bypass access controls.

This is not legal advice. For the longer explanation, see Apify's article [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/).

# Actor input Schema

## `mode` (type: `string`):

Channel posts, one post per URL, or channel info only.

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

Usernames (durov, @durov), channel URLs (https://t.me/durov, https://t.me/s/durov) or post URLs (https://t.me/durov/400).

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

Optional. Uses Telegram's web preview search (?q=).

## `dateFrom` (type: `string`):

YYYY-MM-DD. Paging stops when older posts are reached.

## `dateTo` (type: `string`):

YYYY-MM-DD. Posts newer than this date are skipped. Paging continues toward older posts.

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

Attach the channel title, subscriber count, and description on each post row.

## `includeServiceMessages` (type: `boolean`):

Keep service messages such as the channel-created notice. They are skipped by default.

## `textFormat` (type: `string`):

How to store the post text. textHtml is always the HTML, when the post has text.

## `maxItems` (type: `integer`):

Free version cap: 2,000 per run.

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

Stop paging a channel after this many posts. The run also stops at max results.

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

How many requests can run at once.

## `timeoutSecs` (type: `integer`):

Stop the run after this many seconds and keep the rows already saved.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout.

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

Not needed normally. Enable only if you see 429 or 403.

## Actor input object example

```json
{
  "mode": "posts",
  "channels": [
    "durov",
    "https://t.me/telegram"
  ],
  "searchQuery": "design",
  "includeChannelInfo": false,
  "includeServiceMessages": false,
  "textFormat": "text",
  "maxItems": 100,
  "maxPostsPerChannel": 100,
  "maxConcurrency": 5,
  "timeoutSecs": 300,
  "requestTimeoutSecs": 30,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Posts with date, channel, text, views, reactions, and URL.

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

Channel title, subscriber count, description, and URL.

## `errors` (type: `string`):

Inputs that failed, with the error code and HTTP status.

## `dataset` (type: `string`):

Every row from the run, without a table view applied.

# 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"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("bigibyte/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",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,bigibyte/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/4orG9rZeSr4mbetOY/builds/9uJZ2uMfGNBgLebWn/openapi.json
