# Telegram Channel Scraper - Posts, Views, Reactions, No Login (`mochiboo/telegram-channel-posts-scraper`) Actor

Posts, views, reactions and exact subscriber counts from public Telegram channels. Search inside channels, date filter for daily monitoring, channels mode. Public web preview only: no Telegram account, phone number or API key. E-mails and phones in common formats are replaced (best effort).

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

## Pricing

from $0.40 / 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.

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 and exact subscriber counts from public channels, no login

Pull the posts of any **public Telegram channel** and watch them over time: full text, date, views, reactions (including paid Stars), photos, video thumbnails, files, link previews, links, hashtags, forwards and replies, plus the channel's **exact subscriber count**. You can search inside a channel, fetch only the posts published on or after a date, or switch to a **channels mode** with one row per channel.

It reads the same public page anyone sees in a browser at `t.me/s/<channel>`. **No Telegram account, no phone number, no API key, no login, no cookies.**

### Read this first

- **Public channels only.** Users, bots, groups and private channels (invite links such as `t.me/+AbC...`) have no public preview. They are reported in `FAILURES` (`no_public_preview` or `unsupported_input`) and are never scraped and never charged.
- **No member lists, no comments, no phone numbers.** This actor does not read who subscribed, who commented or who replied. A reply only keeps the link to the post it answers (`replyToUrl`).
- **E-mails and phone numbers in common formats are replaced (best effort)** with `[email redacted]` and `[phone redacted]` in post text, channel description, link-preview text, file names and link addresses. Recognised: e-mail addresses (also spelled-out forms such as `name (at) host.com`), phone numbers that start with `+` or `00`, UK, German, French and US/Canada formats, `wa.me/<number>` and `tel:` links, and Russian and other CIS numbers such as `+7 495 123-45-67`, `8 (495) 123-45-67`, `8 800 555 35 35` and `89123456789`. **Not recognised, so they stay as published:** other countries' national formats written without a `+` (for example India, Brazil, China, Singapore), bare ten- or eleven-digit numbers written without separators (apart from eleven digits that start with 8, such as `89123456789`) and e-mail addresses with non-Latin letters (`ivan@почта.рф`). Channel titles and author signatures are checked for e-mail addresses only, and link-preview site names are not checked. The actor is not a contact-harvesting tool: it never reads user pages or member lists.
- **Views and the preview's subscriber count are rounded by Telegram above 1,000** (it shows `5.82M`). `views` is therefore approximate above 1,000; below that it is exact. `subscribers` is the exact number from the channel's landing page; it is `null` if that page did not answer or you switched `exactSubscribers` off, and `subscribersApprox` (the rounded preview number) is always there.
- **History goes as far back as the channel's web preview allows.** A page holds up to 20 posts, newest first (fewer where messages in between were deleted, hidden or are stickers). The actor follows the preview's own "older posts" link until the channel's first post, your post limit, or your date limit.
- **Posts without text, photo, video, file or link card are skipped and not charged.** That covers service messages (channel photo changed, pinned message, "channel created"), **posts that are only a sticker**, and other posts the web preview cannot show as text or a file (the preview then says "open Telegram to view this post"). They are never delivered and never charged, and the run summary counts them: `skippedService` (service messages), `skippedStickerOnly` (sticker-only posts) and `skippedEmpty` (everything else). A channel that posts only stickers therefore gives 0 rows and a `skippedStickerOnly` count.
- **A refusal from Telegram ends the run.** If Telegram answers 429, 403 or any other refusal, the actor stops, keeps every row already collected and says `blocked` in the run summary. It does not retry, rotate or disguise itself.

### What you get

#### Posts mode (default): one row per post

| field | meaning |
|---|---|
| `postId`, `postUrl` | the post's number and its `https://t.me/<channel>/<id>` link |
| `channelUsername` | the channel as Telegram spells it (the capitalisation can differ from your input) |
| `date` | publication time, ISO with offset (`2026-10-11T14:52:40+00:00`) |
| `edited` | `true` if the post shows the "edited" mark |
| `text` | full text, line breaks kept, e-mails and phones in common formats replaced (best effort) |
| `viewsText`, `views` | views as shown (`1.2M`) and as a number (`1200000`); approximate above 1,000 |
| `reactions`, `reactionsTotal` | each reaction `{emoji, customEmojiId, paidStars, count}` and their sum. Empty list and `0` when the post shows none (also when the channel switched reactions off) |
| `mediaType` | `album`, `video`, `photo`, `document`, `link` or `text` |
| `photoUrls`, `videoCount`, `videoThumbnailUrls` | photo addresses, number of videos, video thumbnail addresses (Telegram CDN links; they can change over time, so keep the files if you need them) |
| `documents` | attached files as `{title, extra}` (file name and size) |
| `linkPreview` | `{url, siteName, title, description}` of the link card, or `null` |
| `links`, `hashtags` | web links and hashtags written in the text |
| `authorSignature` | the author's signature if the channel adds one |
| `isForwarded` | `true` for forwarded posts |
| `forwardedFrom` | `{name, url}`: the original source and the link to the original post, as the preview shows them for a forward from a public channel. `null` when the preview shows no source (forwards out of users and hidden sources) or the post is not a forward |
| `replyToUrl` | link to the post this one replies to |
| `channelTitle`, `channelVerified` | the channel's name and verified badge |
| `subscribers`, `subscribersApprox` | exact subscriber count (`10315764`) and the preview's rounded count (`10300000`) |
| `searchQuery`, `input`, `scrapedAt` | the search you used (or `null`), the channel entry as you typed it, and the time of the run |

#### Channels mode: one row per channel

Set `outputMode` to `channels`. Two requests per channel: `channelUsername`, `channelUrl`, `channelTitle`, `channelDescription`, `channelVerified`, `subscribers` (exact), `subscribersText`, `subscribersApprox`, `photosCount`, `videosCount`, `filesCount`, `linksCount`, `latestPostId`, `latestPostDate`, `postsSampled`, `avgViewsLatest` (average views of the latest posts on the first page), `channelPhotoUrl` (a web address, or `null` for a channel without a photo), `input`, `scrapedAt`. Good for ranking or monitoring many channels cheaply.

### Input

| field | default | meaning |
|---|---|---|
| `channels` (required) | - | usernames or links: `durov`, `@durov`, `https://t.me/durov`, `https://t.me/s/durov`. Duplicates (any capitalisation) are read once |
| `outputMode` | `posts` | `posts` or `channels` |
| `searchQuery` | empty | Telegram's own search inside each channel's public preview (posts mode only) |
| `maxPostsPerChannel` | 100 | newest first, hard cap 5000 (posts mode only) |
| `postedAfter` | empty | `YYYY-MM-DD` (UTC): only posts on or after that day; paging stops at the first older post (posts mode only). **The date is fixed in the saved input, so a schedule repeats it**: every scheduled run delivers everything since that date again. For new posts only, set the date from your own scheduler or API call (for example yesterday's date), or drop repeats by `postUrl` (or `channelUsername` + `postId`) on your side |
| `exactSubscribers` | on | one extra request per channel for the exact subscriber count |
| `maxResults` | 1000 | rows per run across all channels, hard cap 100000 |
| `proxyConfiguration` | Apify datacenter proxy | the default is what this actor was tested with |

Example: `{"channels": ["durov", "https://t.me/telegram"], "maxPostsPerChannel": 100}` returns up to 200 posts. `{"channels": ["durov"], "searchQuery": "ton", "maxPostsPerChannel": 30}` returns the channel's posts that match "ton".

### Example row

```json
{
  "postId": 552,
  "postUrl": "https://t.me/durov/552",
  "channelUsername": "durov",
  "date": "2026-10-11T14:52:40+00:00",
  "edited": false,
  "text": "✨ Designers × AI\n\n🆕 The mockup era is over. ...",
  "viewsText": "1.2M",
  "views": 1200000,
  "reactionsTotal": 29120,
  "reactions": [{"emoji": null, "customEmojiId": null, "paidStars": true, "count": 5950}, {"emoji": null, "customEmojiId": "5936157098181135162", "paidStars": false, "count": 13200}],
  "mediaType": "video",
  "photoUrls": [],
  "videoCount": 1,
  "videoThumbnailUrls": ["https://cdn4.telesco.pe/file/VnHVmW499ucIS1pVBdP5pJ5ag5FLYR8N..."],
  "documents": [],
  "linkPreview": null,
  "links": ["https://t.me/designers"],
  "hashtags": [],
  "authorSignature": "Pavel Durov",
  "isForwarded": true,
  "forwardedFrom": null,
  "replyToUrl": null,
  "channelTitle": "Pavel Durov",
  "channelVerified": true,
  "subscribers": 10315764,
  "subscribersApprox": 10300000,
  "searchQuery": null,
  "input": "durov",
  "scrapedAt": "2026-10-20T10:00:00.000Z"
}
```

(The text, reactions and thumbnail address are shortened here; real rows carry the full values.)

### Speed, limits and how a run ends

- One request every 2 seconds, one after the other, plus the time to fetch and read each page. A page holds up to 20 posts. **Measured on the platform on 21 Oct 2026 at 512 MB of memory** on two big news channels: about 3 seconds per page (runs of 32 and 52 requests: 2.9 and 3.0 seconds per request, the 2-second pause included), so 100 posts take about 15 to 20 seconds when every page holds 20 posts; a third, small run took 8.7 seconds per page when Telegram answered slowly. Channels with deleted, hidden or sticker-only messages have fewer posts per page (7 pages for 28 posts and 10 pages for 40 posts in two small channels), so allow more requests there. The figure for 5,000 posts (about 250 pages, 13 to 20 minutes per channel) is an estimate, not a measured run. One more request per channel asks for the exact subscriber count. Less memory is slower, because reading the pages needs CPU.
- A very large run (many channels with a high `maxPostsPerChannel`) can take hours. Raise the run timeout for those. The actor stops starting new requests shortly before the timeout, keeps every row already collected and reports `time_budget` in the run summary, so you can see that the result is partial.
- `RUN_SUMMARY` (key-value store record) is written at the end of every run that reads Telegram: `{complete, terminationReason, rowsPushed, channels, noPreview, skippedService, skippedEmpty, skippedStickerOnly, requests}`. A run that stops at once because no input was usable writes only `FAILURES`. If the platform restarted the run, `restarts` is added as the last key.
  - `terminationReason`: `finished` (everything asked for was read), `max_results` (your `maxResults` was reached), `charge_limit` (your maximum cost per run was reached), `time_budget` (the run timeout was near), `blocked` (Telegram refused a request), `request_failed` (a page could not be read after two retries, so the result is partial). `complete` is `true` only for `finished` and `max_results`.
  - `channels` counts channels whose preview was read, `noPreview` counts inputs without a public preview.
- `FAILURES` (key-value store record) lists the inputs that could not be read: `{input, channel, status, httpStatus}` with `status` one of `unsupported_input` (not a public channel username or t.me link, including private invite links), `no_public_preview` (a user, bot, group, unknown name or a channel without a public preview), `not_found` (HTTP 404), `request_failed` (server error or timeout after two retries), `blocked` (Telegram refused the request). A channel that was read but has nothing to deliver (only stickers, no search match, nothing newer than `postedAfter`) is not listed: check `RUN_SUMMARY` (`rowsPushed`, `skippedStickerOnly`). A channel that gave rows can also appear here when a later page failed.

### Pricing

Pay per result, at the rate shown in the pricing panel of this page, plus a very small per-run start fee. You are charged only for the results delivered.

Only delivered rows are charged: channels without a public preview, service messages, empty messages and failed requests cost nothing.

### Good to know

- The actor reads Telegram's public web preview. If Telegram changes that page, fields can become empty before this actor is updated; the run summary and `FAILURES` show it.
- Channels can hide older posts, delete posts or switch reactions off; you get what the preview shows on the day of the run.
- If Apify moves the run to another server, the run restarts: it reads its pages again but never writes (or charges) a post it already delivered. A restart waits about 12 seconds before it continues, and the rows still come out once.
- Use it for channel monitoring, crypto and news channel tracking, competitor and brand mentions, content research and subscriber-count tracking over time. For alerts, schedule a run with `searchQuery` and set `postedAfter` yourself (a schedule repeats a fixed date), or drop repeats by `postUrl`.

Keywords: telegram scraper, telegram channel scraper, telegram posts scraper, telegram messages, telegram channel monitoring, telegram subscribers count, t.me scraper, crypto telegram channels, telegram analytics, no telegram api.

# Actor input Schema

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

Channel usernames or links, e.g. durov, @durov, https://t.me/durov or https://t.me/s/durov. Public channels only.

## `outputMode` (type: `string`):

Posts: one row per post. Channels: one row per channel with its title, description, exact subscribers, media counters and latest post.

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

Telegram's own search inside each channel's public preview. Leave empty for all posts.

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

Newest first. Hard cap 5000.

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

YYYY-MM-DD (UTC). Paging stops at the first older post. The date stays fixed in the saved input, so a schedule repeats it: for new posts only, move the date yourself or drop repeats by postUrl.

## `exactSubscribers` (type: `boolean`):

One extra request per channel to t.me/<channel> for the exact number (the preview shows it rounded, e.g. 10.3M).

## `maxResults` (type: `integer`):

Hard cap 100000.

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

Apify Proxy settings. The default shared datacenter proxy is what this actor was tested with.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "https://t.me/telegram"
  ],
  "outputMode": "posts",
  "searchQuery": "",
  "maxPostsPerChannel": 100,
  "postedAfter": "",
  "exactSubscribers": true,
  "maxResults": 1000,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per post (or per channel in channels mode): text, date, views, reactions, media, links and exact subscribers.

## `runSummary` (type: `string`):

What the run wrote and how it ended (the RUN_SUMMARY record in the default key-value store).

# 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",
        "https://t.me/telegram"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("mochiboo/telegram-channel-posts-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",
        "https://t.me/telegram",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

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

```

## MCP server setup

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