# Telegram Scraper · Channel Posts by Date, Views & Reactions (`thequietstack/telegram-channel-scraper`) Actor

Scrape public Telegram channels without login: posts in an exact from/to date window (jumps straight to old dates), views, per-emoji reactions, forwards, replies, media and links, keyword filter, only-new mode. Channels without a public preview are reported, never charged.

- **URL**: https://apify.com/thequietstack/telegram-channel-scraper.md
- **Developed by:** [TheQuietStack](https://apify.com/thequietstack) (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 $2.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 Scraper · Channel Posts by Date, Views & Reactions

**Telegram scraper for public channels:** posts from an exact date range with views, reactions, forwards, media and links, without login or phone number.

Scrape **public Telegram channels** from Telegram's own public web preview (`t.me/s/<channel>`): every post becomes one flat row with its ISO date, text, views, **reactions per emoji**, forward source, reply target, media type and media URLs, links, hashtags and an edited flag. No login, no Telegram account, no API key, no phone number.

Before you use the output, read [Personal data & legal use](#personal-data--legal-use).

### Why this scraper

- **A date range that is exact - in both directions.** Set `dateFrom` and `dateTo` (a day or an ISO date-time with offset). Every post is checked against both bounds; paging stops at the first post older than `dateFrom`. Posts outside the window are never written and never charged.
- **Old windows without paging through everything newer.** If `dateTo` lies months or years back, the Actor jumps there by binary search over post IDs (`?before=`) instead of walking every page in between. Measured on 01 Oct 2026: a window ending 31 Mar 2026 was found with 4 extra page requests on `durov` and 7 on `telegram`.
- **Every channel gets a clear verdict.** A username that has no public preview is not silently skipped: the run summary says whether it does not exist, is a bot or user, is a group, or is a channel whose preview Telegram redirects away from this server (typically a regional or legal restriction). **None of these is charged.**
- **Honest numbers.** The preview abbreviates counts (`1.85M`). You get the converted number in `views` *and* the shown text in `viewsText`, plus `viewsExact: false` whenever Telegram rounded. The channel profile record takes the **exact subscriber count** from the channel page (`10 563 281`, not `10.6M`).
- **Monitoring mode.** `onlyNewPosts` remembers the newest post per channel and on the next scheduled run returns only what is newer - you are not charged twice.
- **A hard limit you can trust.** `maxPosts` and Apify's *Max cost per run* stop the run cleanly; nothing beyond is fetched or charged.

### Input example

```json
{
    "channels": ["durov", "https://t.me/telegram"],
    "dateFrom": "2025-06-01",
    "dateTo": "2026-03-31",
    "maxPostsPerChannel": 500,
    "keywords": ["update"],
    "includeChannelProfile": true
}
```

- `channels` - usernames, `@names`, `t.me/<name>`, `t.me/s/<name>` or post links. Invite links (`t.me/+...`) belong to private chats and are rejected.
- `dateFrom` / `dateTo` - inclusive. A plain date means 00:00:00 / 23:59:59.999 **UTC** of that day; give an offset (`2026-09-01T00:00:00+02:00`) for another time zone. `daysBack` is a shortcut for `dateFrom`.
- `keywords` + `keywordMode` (`any` / `all`) - case-insensitive match on the post text. `skipForwarded` drops forwarded posts. Filtered posts are not charged.
- `maxPostsPerChannel`, `maxPosts` (whole run), `maxPagesPerChannel` (safety cap, 20 posts per page).
- `includeChannelProfile` (+ `exactSubscriberCount`) - one extra row per channel.
- `onlyNewPosts` + `stateStoreName` - for schedules.
- `requestDelaySecs` - pause between page requests, at least 1 s (default 2 s).

### Output example

```json
{
    "recordType": "post",
    "channel": "durov",
    "channelTitle": "Pavel Durov",
    "postId": 548,
    "url": "https://t.me/durov/548",
    "date": "2026-09-11T16:04:02.000Z",
    "text": "🤝 Telegram has become the sponsor of Codeforces — the largest competitive programming platform in the world. ...",
    "views": 1850000,
    "viewsText": "1.85M",
    "viewsExact": false,
    "reactions": [
        { "emoji": "⭐", "customEmojiId": null, "paid": true, "count": 13900, "countText": "13.9K", "countExact": false },
        { "emoji": null, "customEmojiId": "5399847211989246390", "paid": false, "count": 30100, "countText": "30.1K", "countExact": false }
    ],
    "forwardedFrom": null,
    "replyTo": null,
    "mediaType": "text",
    "media": [],
    "mediaUrls": [],
    "linkPreview": { "url": "https://codeforces.com/blog/entry/156620", "siteName": "Codeforces", "title": "Telegram Returns as Title Sponsor of Codeforces!", "description": "Hi, Codeforces!", "imageUrl": "https://cdn4.telesco.pe/file/..." },
    "links": ["https://codeforces.com/blog/entry/156620"],
    "hashtags": [],
    "mentions": [],
    "edited": false,
    "authorSignature": "Pavel Durov",
    "scrapedAt": "2026-10-01T12:00:00.000Z"
}
```

| Field | Meaning |
|---|---|
| `postId`, `url`, `date` | Post ID within the channel, permalink, publication time in ISO 8601 UTC (edits keep the original date) |
| `views`, `viewsText`, `viewsExact` | Number as shown, converted (`1.2K` -> 1200); `viewsExact` is `false` when Telegram abbreviated it |
| `reactions` | One entry per reaction: `emoji` (standard emoji), `customEmojiId` (custom emoji, which the preview shows only as an ID), `paid` (Telegram Stars), `count` / `countText` / `countExact` |
| `forwardedFrom` | `{ name, url, channel, postId }` of the original, when the post is a forward |
| `replyTo` | `{ postId, channel, url, author, text }` of the replied-to post |
| `mediaType` | `text`, `photo`, `video`, `round_video`, `gif`, `voice`, `audio`, `document`, `sticker`, `poll`, `location`, `album`, `mixed`, `none` |
| `media`, `mediaUrls` | Per item: type, URL, thumbnail, duration, title; `mediaUrls` is the flat list of downloadable URLs |
| `linkPreview`, `links`, `hashtags`, `mentions` | Link card and everything linked in the text |
| `edited`, `authorSignature` | Edited marker and the author signature, if the channel shows one |

With `includeChannelProfile`, one row per channel has `recordType: "channel"`: `title`, `description`, `subscribers` (+ `subscribersText`, `subscribersExact`), `photoUrl`, `verified`, `photos`, `videos`, `files`, `links`, `url`.

The run summary (`SUMMARY` in the key-value store) lists per channel: status (`ok`, `partial`, `no_public_preview`, `not_found`, `not_a_channel`, `failed`, `blocked`), the reason, posts written, pages fetched, date-seek pages, what was skipped (after `dateTo`, keyword, forwarded, service messages, duplicates, already seen) and why paging stopped.

### Honest limits

- **Public channels only.** Groups, private channels, user accounts and bots have no public web preview. They are reported in the summary, never charged.
- **Counts above 999 are rounded by Telegram** in the preview (`views`, reaction counts). We convert them and flag them (`viewsExact: false`) - we never present a rounded number as exact. Only the subscriber count has an exact public source, and the profile record uses it.
- **Custom emoji reactions** appear in the preview only as an ID (`customEmojiId`), not as a character.
- **Media URLs** point to Telegram's CDN and are signed; they expire after some time. Download what you need soon after the run. Large videos the preview does not play come with a thumbnail and `url: null`.
- **Regional restrictions:** some channels are not shown to servers in certain regions; Telegram then redirects the preview away. The Actor reports this as `no_public_preview` and does not try to get around it.
- The date jump assumes post IDs grow with time, which is how Telegram numbers channel posts. Every written post is still checked against both bounds individually.
- Comments under posts are not part of the public preview and not part of this Actor.

### Polite access - Bot protection is never bypassed

- One request at a time, with a pause between pages (`requestDelaySecs`, minimum 1 s, default 2 s), and hard caps per channel and per run.
- **Bot protection is never bypassed.** If Telegram answers with HTTP 403 or 429, a captcha or challenge page, a redirect to a login page, or HTML without the expected preview structure, the Actor stops the whole run at once with status FAILED and a clear message (`SUMMARY.blocked` names the channel and the reason; remaining channels are listed under `notReached`). No retries, no proxy or IP rotation, no header tricks, no login. Rows written before the stop stay in the dataset; nothing is charged for the blocked page or anything after it.
- Network errors and other HTTP errors are not retried either: the channel is marked `failed` or `partial` and the run moves on.

### Personal data & legal use

Public channel posts are public, but they can still contain personal data (names, photos, opinions, author signatures, forwarded posts of private persons).

- **You are the controller.** Under the Apify Standard Actor Contract (section 5.2.1) the user who runs this Actor is the controller of the personal data it processes; the Actor's creator acts as processor. You need your **own legal basis** for your purpose and must give the people concerned the information the law requires.
- **Users in the EU/EEA** (or processing data of people there) are themselves subject to the **GDPR**, including Art. 6 (legal basis), Art. 14 (information when data is not collected from the person) and Art. 21 (right to object). Collect only what your purpose needs and delete it when it is no longer needed.
- **Not allowed:** stalking, profiling or tracking individuals, harassment, and building contact lists for unsolicited messages.
- **No enrichment, no accounts.** The output contains only what Telegram's public preview shows. For usernames that belong to a person, the Actor records nothing but the status `not_a_channel`.
- **Nothing is kept between runs** except, with `onlyNewPosts`, the newest post ID per channel in your own key-value store.
- Respect Telegram's Terms of Service and the rights of channel owners when you reuse texts or media.

### Pricing (pay per event)

- **Per post** (`post`): charged only for posts written to the dataset.
- **Per channel profile** (`channel-profile`): only with `includeChannelProfile`.
- Never charged: channels without a public preview, non-existent or invalid usernames, blocked or failed pages, posts outside the date window, keyword misses, skipped forwards, service messages, duplicates, and posts already delivered by an earlier `onlyNewPosts` run.

### Use it via API, integrations and AI agents

```bash
curl -X POST "https://api.apify.com/v2/acts/thequietstack~telegram-channel-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"channels":["durov"],"daysBack":30,"maxPostsPerChannel":100}'
```

Works with Apify integrations (Make, n8n, Zapier, Google Sheets), schedules and webhooks.

### Data source and legal

- **Source:** Telegram's public channel web preview at `https://t.me/s/<channel>` and the public channel page `https://t.me/<channel>` (exact subscriber count). No login, no Telegram API, no API key.
- This Actor is not affiliated with or endorsed by Telegram.

# Actor input Schema

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

Public channel usernames or links: durov, @durov, https://t.me/durov, https://t.me/s/durov or a post link like https://t.me/durov/123. Invite links (t.me/+...) are private and not supported.

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

YYYY-MM-DD (start of that day, UTC) or an ISO date-time with offset, e.g. 2026-09-01T00:00:00+02:00. Paging stops at the first older post.

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

YYYY-MM-DD (until the end of that day, UTC) or an ISO date-time. If this lies deep in the past, the Actor jumps there directly (binary search over post IDs) instead of paging through everything newer.

## `daysBack` (type: `integer`):

Shortcut for dateFrom = now minus N days. Ignored when dateFrom is set.

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

Stop each channel after this many written posts.

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

The run stops cleanly at this number. You are never charged for more posts than this.

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

Keep only posts whose text contains these words (case-insensitive). Posts that do not match are not charged.

## `keywordMode` (type: `string`):

Whether a post needs one or all of the keywords.

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

Drop posts forwarded from other chats. Skipped posts are not charged.

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

One extra row per channel (recordType "channel"): title, description, subscribers, photo URL, verified flag, media counters. Charged as one channel-profile event.

## `exactSubscriberCount` (type: `boolean`):

The preview shows "10.6M"; the channel page shows the exact number (e.g. 10 563 281). Costs one extra page request per channel. Only used with the profile record.

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

For scheduled monitoring: remembers the newest post ID per channel in a named key-value store and stops paging there on the next run. You are not charged again for posts you already have.

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

Named key-value store for the only-new-posts memory. Use a different name per monitoring task.

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

Safety cap. One page holds up to 20 posts. Pages skipped by the date jump do not count.

## `requestDelaySecs` (type: `integer`):

Pages are fetched one at a time with this pause. Lower is not allowed: this Actor crawls politely.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "https://t.me/telegram"
  ],
  "maxPostsPerChannel": 100,
  "maxPosts": 1000,
  "keywords": [
    "update",
    "bitcoin"
  ],
  "keywordMode": "any",
  "skipForwarded": false,
  "includeChannelProfile": false,
  "exactSubscriberCount": true,
  "onlyNewPosts": false,
  "stateStoreName": "telegram-channel-scraper-state",
  "maxPagesPerChannel": 200,
  "requestDelaySecs": 2
}
```

# Actor output Schema

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

One row per post: channel, post ID, URL, ISO date, text, views, reactions per emoji, forward source, reply target, media type and URLs, links, hashtags, edited flag. Channel profile rows carry recordType "channel".

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

Per channel: status (ok, no\_public\_preview, not\_found, not\_a\_channel, failed, blocked), posts written, pages fetched, what was skipped and why paging stopped. Nothing listed as skipped is charged.

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

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

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

```

## MCP server setup

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