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

Scrape posts from public Telegram channels: text, views, reactions, media, polls, forwards and replies. Monitor many channels with only-new-posts mode, date ranges, in-channel search and a webhook.

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

## Pricing

from $1.00 / 1,000 channel 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 Public Channel Scraper

Turn any public Telegram channel into clean rows of posts with views, reactions, media, polls, forwards and replies, and keep watching it with an only-new-posts mode that never charges you twice for the same post.

### What does Telegram Public Channel Scraper do?

You give it a list of public channels (`durov`, `@cointelegraph`, `t.me/s/bbcrussian` or a post link). For each channel it reads Telegram's own public web preview at `t.me/s/<channel>`, the same page anyone can open in a browser without logging in, and pages back through the history newest first. Every post becomes one row with the full text, publish date, view count, the breakdown of emoji reactions, photos, videos, documents, audio, stickers, polls with percentages, link previews, links, hashtags, cashtags and mentions. Turn on **Only new posts**, schedule it, and each run returns just what was published since the last run, optionally pinging Slack or any webhook.

### Why use it

- **Monitoring built in, not bolted on.** Incremental sync stores the last post ID per channel in a named key-value store. Scheduled runs return only new posts, and runs cut short by your spending limit resume where they stopped, without charging again for posts you already received.
- **Full engagement metrics.** Views parsed to numbers (`1.57M` → `1570000`, raw value kept), every reaction with its count (standard emoji, custom emoji by ID, and paid Telegram Stars counted separately), poll voters and option percentages.
- **Many channels in one run.** Paste hundreds of channels. They run in parallel, each channel's rows stay together, newest first, and every channel gets one free info row with the exact subscriber count (`10,360,902`, not just `10.4M`).
- **Search inside channels, deeper than Telegram.** Use Telegram's own in-channel search for keywords, `#hashtags` or `$cashtags`. Telegram's web search stops at about 20 results per channel; turn on **Deep search** and the Actor pages through the history and matches the text itself, so you get every matching post in your date range (82 matches for one query in a test where Telegram's search returned 22).
- **Date ranges that don't waste time.** `dateFrom` stops paging as soon as older posts appear. For an old `dateTo`, the Actor jumps straight to that date with a binary search over post IDs (about 10–15 requests even on channels with 100,000 posts) instead of paging through everything newer.
- **Clear answers instead of empty runs.** Channels that don't exist, have the web preview disabled, are invite-only, or are actually groups, bots or users get a free status row that says exactly why.
- **Albums handled properly.** A grouped album (several photos, videos or files sent together) is one post with all its media, not five half-empty rows.
- **Fair pricing.** $1 per 1,000 posts. Channel rows, status rows, service messages ("Channel created", pinned notices), filtered-out posts, already-seen posts and posts the web preview cannot render are free.

### What data you get

One row per post (`type: "post"`), plus one free channel row per channel (`type: "channel"`) and free status rows (`type: "status"`) for channels without readable posts.

**Post rows**

| Field | Description |
|---|---|
| `channel`, `channelTitle` | Channel username and display name |
| `postId`, `url` | Post number in the channel and its `https://t.me/<channel>/<id>` link |
| `date` | Publish time, ISO 8601 UTC |
| `text` | Plain text with line breaks (emoji kept). `textHtml` adds the formatted HTML when enabled |
| `views`, `viewsRaw` | View count as a number, and as Telegram shows it (`41.2K`) |
| `reactions[]` | `{ emoji, type, count }`. `type` is `emoji`, `custom_emoji` (with `customEmojiId`; `emoji` is `custom:<id>` when Telegram shows no fallback) or `paid` (Telegram Stars, `⭐`) |
| `totalReactions`, `paidStars` | Sum of all emoji reactions, and Stars sent as paid reactions |
| `isForwarded`, `forwardedFrom` | `{ name, url, channel, postId }` of the original source when Telegram shows it |
| `replyTo`, `replyToUrl` | ID and link of the post this one replies to |
| `authorSignature` | Admin signature, on channels that sign posts |
| `isEdited`, `viaBot` | Edited flag, and the inline bot used to send the post |
| `media[]`, `mediaTypes` | Each item: `type` (`photo`, `video`, `document`, `audio`, `voice`, `sticker`, `poll`, `link_preview`, `location`), plus `url`, `thumbnail`, `duration` (seconds), `fileName`, `fileSize`, `title`/`performer` for audio, `animated` for GIF-style videos and animated stickers, and `postId` for album items |
| `albumPostIds` | All post IDs that belong to a grouped album |
| `linkPreview` | `{ url, siteName, title, description, imageUrl }` |
| `links[]`, `hashtags[]`, `cashtags[]`, `mentions[]` | Extracted from the post text |
| `poll` | `{ question, type, voters, options: [{ text, percent }] }` |
| `contentUnavailable` | `true` when the web preview shows only "open in Telegram" (stories, giveaways, paid media). Such rows keep date, views and reactions and are **not charged** |
| `isNew` | `true` in only-new-posts mode |
| `searchQuery`, `input`, `scrapedAt` | The search used (Telegram search or deep search), the input line, and the scrape time |

**Channel rows**: `channel`, `title`, `description`, `subscribers` (exact count from the channel's profile page), `subscribersRaw`, `photosCount`, `videosCount`, `filesCount`, `linksCount` (as shown in the channel header), `avatarUrl`, `isVerified`, `isScam`, `isFake`, `lastPostId`, `lastPostDate`, `url`, `previewUrl`.

**Status rows**: `status` is `not_found`, `preview_disabled`, `private` (invite links and `t.me/c/...`), `not_a_channel` (groups, bots, user accounts) or `failed`, with a human-readable `message`.

The run's key-value store also has a `SUMMARY` record with one entry per channel: posts pushed, newest and oldest post ID, pages fetched, why paging stopped (`max_posts`, `date_from`, `last_seen_post`, `end_of_channel`, `charge_limit`) and whether it was a baseline run.

### How to use

1. Paste channels into **Channels**, one per line: `durov`, `@durov`, `t.me/durov`, `https://t.me/s/durov` or a post link like `t.me/durov/123`.
2. Set **Max posts per channel**. It caps both the result size and the cost per channel.
3. Optionally set **Posts from** / **Posts until** (dates or relative values like `7 days`) and a **Search within each channel** query. Turn on **Deep search** when you need more than Telegram's ~20 most recent matches per channel.
4. For monitoring, turn on **Only new posts since the last run** (see the recipe below).
5. Click **Start**, then open the **Posts** view and export it as CSV, Excel or JSON.

```json
{
  "channels": ["durov", "@cointelegraph", "https://t.me/s/WatcherGuru", "t.me/bbcrussian"],
  "maxPostsPerChannel": 100,
  "dateFrom": "2026-10-01",
  "searchQuery": "",
  "includeChannelInfo": true,
  "onlyNewPosts": false
}
```

### Output example

Real rows from a local test run (long text and CDN URLs trimmed):

```json
{
  "type": "post",
  "channel": "durov",
  "channelTitle": "Pavel Durov",
  "postId": 531,
  "url": "https://t.me/durov/531",
  "date": "2026-07-16T20:45:25.000Z",
  "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": 5810000,
  "viewsRaw": "5.81M",
  "reactions": [
    { "emoji": "⭐", "type": "paid", "count": 6300 },
    { "emoji": "custom:5265077361648368841", "type": "custom_emoji", "customEmojiId": "5265077361648368841", "count": 54600 },
    { "emoji": "custom:5465587407350942612", "type": "custom_emoji", "customEmojiId": "5465587407350942612", "count": 16400 }
  ],
  "totalReactions": 88790,
  "paidStars": 6300,
  "isForwarded": false,
  "forwardedFrom": null,
  "replyTo": null,
  "authorSignature": "Pavel Durov",
  "isEdited": false,
  "mediaTypes": ["video"],
  "media": [
    { "type": "video", "url": "https://cdn4.telesco.pe/file/b231b9b161.mp4?token=...", "thumbnail": "https://cdn4.telesco.pe/file/...", "duration": null, "animated": true }
  ],
  "linkPreview": null,
  "links": [],
  "hashtags": [],
  "cashtags": [],
  "mentions": [],
  "poll": null,
  "input": "durov",
  "scrapedAt": "2026-10-16T11:34:10.176Z"
}
```

A poll post (Telegram X channel):

```json
{
  "type": "post",
  "channel": "tgx_android",
  "postId": 311,
  "date": "2024-06-13T15:25:56.000Z",
  "views": 360000,
  "totalReactions": 7070,
  "paidStars": 3,
  "mediaTypes": ["poll"],
  "poll": {
    "question": "Choose only one feature you wish to see in the next Telegram X update if nothing else would be added. Which one is it going to be?",
    "type": "Anonymous Poll",
    "voters": 48700,
    "options": [
      { "text": "Bot Platform update / Support for new bots", "percent": 29 },
      { "text": "Topics in group chats", "percent": 12 },
      { "text": "Stories", "percent": 8 }
    ]
  }
}
```

A free channel row and a free status row:

```json
{
  "type": "channel",
  "channel": "cointelegraph",
  "title": "Cointelegraph",
  "subscribers": 330486,
  "subscribersRaw": "330K",
  "photosCount": 20800,
  "videosCount": 1290,
  "filesCount": 4,
  "linksCount": 58400,
  "isVerified": false,
  "lastPostDate": "2026-10-16T09:23:50.000Z",
  "url": "https://t.me/cointelegraph"
}
```

```json
{
  "type": "status",
  "status": "not_a_channel",
  "message": "@tdlibchat is a group, not a channel. Only public channels have a web preview with posts.",
  "channel": "tdlibchat",
  "title": "TDLib chat",
  "input": "tdlibchat"
}
```

### Monitoring recipe: schedule + only new posts + webhook

1. Turn on **Only new posts since the last run**. Keep the default **State store name** (`telegram-channel-state`) or pick one per watchlist, for example `crypto-watchlist`.
2. Run it once. This is the baseline: it returns up to **Max posts per channel** recent posts per channel and remembers the newest post ID.
3. In Apify Console, add a **Schedule** (for example every hour or every morning) with the same input.
4. Optional: paste a Slack incoming-webhook URL, or a Zapier, Make or n8n URL, into **Webhook URL for new posts**. After every run with at least one new post, the Actor POSTs a JSON summary with a Slack-ready `text` line per channel and up to 20 latest posts per channel (URL, date, views, text snippet). Runs without new posts don't call the webhook.

Each later run pages back only until it reaches the last post it already saw, so a quiet channel costs one request and nothing else. Posts outside your date range or search still advance the baseline. A search query keeps its own baseline, so you can watch `durov` and `durov` + `#update` side by side. If more than **Max posts per channel** new posts appeared between two runs, you get the newest ones and the baseline moves on; raise the limit or run more often for very busy channels. If a run stops at your spending limit, the posts it could not deliver are returned by the next run, and the ones it did deliver are not repeated.

### Pricing

**$0.001 per post row** (event `channel-post`), which is **$1.00 per 1,000 posts**.

| Run | Posts returned | Cost |
|---|---|---|
| 10 channels × 100 latest posts | 1,000 | $1.00 |
| Research dataset: 5 channels × 2,000 posts | 10,000 | $10.00 |
| Search `$TON` in 20 crypto channels, about 20 matches each | 400 | $0.40 |
| Hourly monitoring of 50 channels, about 8 new posts per channel per day | 400 per day | $0.40 per day (about $12 per month, after the baseline run) |

**Not charged:** channel info rows, status rows (not found, preview disabled, private, not a channel, failed), service messages, posts outside your date range or search, posts already returned in only-new-posts mode, posts the web preview cannot display (`contentUnavailable`), and the webhook. **Max posts per channel** caps the spend per channel. If you set a maximum cost per run, the Actor stops cleanly when it is reached and the next monitoring run continues from there.

### Integrations / scheduling

- **Schedules:** hourly or daily runs with only-new-posts mode give you a channel feed for alerts, dashboards or archives.
- **Webhooks:** the built-in webhook posts new-post summaries to Slack, Zapier, Make or n8n. Apify's own run webhooks can trigger any downstream job when a run finishes.
- **API:** start runs and read results from any language with the Apify API or the JavaScript and Python clients. Use `?view=posts` for the compact table.
- **MCP and AI agents:** call the Actor as a tool through the Apify MCP server, so an agent can answer "what did these 20 crypto channels say about ETF approvals this week?" with real posts and engagement numbers.

### Use cases

- **Crypto and trading signals:** track announcement and alpha channels, `$cashtags` and reaction spikes within minutes of posting.
- **News monitoring:** follow newsrooms and regional outlets that publish on Telegram first, in any language.
- **Brand and product monitoring:** search public channels for your brand, product or competitors and get alerted when they are mentioned.
- **OSINT and research:** build timestamped datasets of public channel activity with forward chains (`forwardedFrom`) and reply threads for analysis.
- **Research datasets and AI:** export clean text plus engagement for NLP, sentiment analysis or RAG pipelines.

### FAQ & limitations

**Which channels work?** Public channels that have a username and a web preview at `t.me/s/<name>`. Private channels, invite-only links, groups, bots and user accounts are not readable this way and get a free status row. Some channels have the web preview turned off (by the owner or restricted by Telegram); they get a `preview_disabled` row with the title and subscriber count.

**Are views and reactions exact?** Telegram's web preview rounds large numbers (`41.2K`, `5.81M`). The Actor converts them to numbers and also keeps the original text in `viewsRaw`. Channel subscriber counts are exact because they come from the channel's profile page.

**What about custom emoji reactions?** Telegram doesn't show the picture of premium custom emoji in the web preview, so they are reported by their stable `customEmojiId`. Standard emoji are returned as Unicode.

**Forwarded posts.** `isForwarded` is always reliable. The source name and link are returned when Telegram includes them in the preview; occasionally the preview leaves the source name blank, and then `forwardedFrom` fields are `null`.

**Media URLs.** Photo, video, sticker and audio links point to Telegram's CDN and are signed, temporary URLs. Download the files soon after the run if you need to keep them. Very large videos have only a thumbnail in the preview, and documents link to the post rather than the file.

**How deep does search go?** Telegram's web search returns only about the 20 most recent matching posts per channel and query. **Deep search** removes that limit by scanning the channel page by page (about 1 second per 20 posts, up to 10,000 posts per channel) and matching locally: every word or `"quoted phrase"` must appear in the text, link preview, poll or file name, case-insensitive. Set **Posts from** to bound the scan. Only matching posts are charged.

**Comments.** Discussion-group comments under channel posts are not part of the public web preview and are not collected.

**Speed.** About 20 posts per request. Each channel is paged at most once per second and requests to Telegram are spaced out, so 1,000 posts from one channel take about a minute, and many channels run in parallel.

**Responsible use.** The Actor reads only public channel content that Telegram itself publishes on the open web, without logging in. It does not collect members, subscribers' identities, private messages or data from private chats or groups. Respect Telegram's Terms of Service, the channel owners' rights and your local law (including the GDPR when posts mention people) when you store, analyze or republish posts, and link back to the original post URL when you show content to others.

# Actor input Schema

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

One public channel per line. Accepts a username (durov), @durov, t.me/durov, https://t.me/s/durov or a post link like t.me/durov/123 (the channel is taken from it). Only public channels with a web preview work. Private invite links, groups, bots and users get a free status row explaining why.

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

Upper limit of post rows per channel, newest first. This caps your cost: each post row is one charged event. Telegram shows about 20 posts per page, so 100 posts take about 5 page requests (about 5 seconds) per channel.

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

Only posts published on or after this date (UTC). Paging stops as soon as older posts are reached, so a recent date makes runs faster and cheaper. Use YYYY-MM-DD, a full ISO timestamp, or a relative value like 7 days.

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

Only posts published on or before this date (UTC, a date-only value includes that whole day). For old date ranges the Actor jumps back with a few probe requests (binary search by post ID) instead of paging through every newer post. Leave empty in monitoring mode.

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

Optional. Returns only posts that match this text, using Telegram's own in-channel search (the same as the search box on t.me/s/name). Works with words, phrases, #hashtags and $cashtags, for example bitcoin, #AI or $TON. Leave empty for all posts.

## `deepSearch` (type: `boolean`):

Telegram's built-in search returns only the ~20 most recent matches per channel. Turn this on to page through the channel history instead and match the search text locally (case-insensitive, every word or "quoted phrase" must appear in the text, link preview, poll or file name). Only matching posts are charged, but scanning is slower: about 1 second per 20 posts, at most 10,000 posts per channel. Combine with Posts from to bound the scan.

## `onlyNewPosts` (type: `boolean`):

Monitoring mode. The newest post ID per channel is stored in the named key-value store below. Each run outputs (and charges) only posts newer than that. The first run is the baseline: it returns up to Max posts per channel and remembers where it stopped. Schedule the Actor with the same input to get a feed of new posts.

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

Named key-value store that keeps the last seen post ID per channel between runs (only used with Only new posts). Use a different name per watchlist or schedule to keep separate baselines. A search query gets its own baseline automatically. Lowercase letters, digits and hyphens.

## `webhookUrl` (type: `string`):

Optional. After the run, a JSON summary (posts per channel, up to 20 latest posts each with URL, date, views and text snippet) is POSTed here, only when at least one post was returned. The payload has a Slack-compatible text field, so a Slack incoming webhook works directly. Not charged.

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

Adds one free row per channel with title, description, exact subscriber count, photo/video/file/link counters, avatar, verified flag and the latest post date. Costs one extra request per channel.

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

Adds textHtml (the formatted post body with bold, links and custom emoji markup) next to the plain text. Makes the dataset larger.

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

How many channels are scraped at the same time. Each channel is paged at most once per second and all requests to t.me are spaced out, so higher values mainly help with long channel lists.

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

Optional. The public web preview usually works without a proxy. A proxy spreads requests over several IPs for large channel lists or when t.me rate-limits your IP. Leave empty to use direct requests.

## Actor input object example

```json
{
  "channels": [
    "telegram",
    "durov"
  ],
  "maxPostsPerChannel": 20,
  "deepSearch": false,
  "onlyNewPosts": false,
  "stateStoreName": "telegram-channel-state",
  "includeChannelInfo": true,
  "includeTextHtml": false,
  "maxConcurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

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

No description

## `results` (type: `string`):

No description

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

No description

# 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",
        "durov"
    ],
    "maxPostsPerChannel": 20,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

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

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

```

## MCP server setup

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