# Telegram Channel Messages (`feedminer/telegram-channel-messages`) Actor

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

## Pricing

from $0.18 / 1,000 post delivereds

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?

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

<div style="border-left:3px solid #1C1917;background:#FAFAF9;padding:15px 20px;border-radius:3px">
<strong>A list of Telegram channels in, one row per public post out.</strong> Date, full text, view count, reactions with their per-emoji breakdown, what the post forwards or answers, its attachment and the CDN links to it, polls, the unfurled link card, and every URL the post points at. Bounded by a date and a per-channel cap. Delivered as JSON, CSV or Excel, or read straight from the API. No Telegram account of yours is involved, and nothing has to be connected.
</div>

🔓 **Nothing to connect.** No Telegram login, no phone number, no bot token, no API key, no cookies to paste. Every read is a public `t.me` page Telegram serves to anyone, so there is nothing in the loop that can expire on you.

⚡ **9 704 posts from 10 channels in under three minutes.** Measured on platform run `2kxvM6LRqgi4IRBmu`, at the 256 MB this Actor asks for — 561 pages of history walked, eight channels at a time, nothing refused.

💬 **Twenty-three columns, all read off one page.** The reactions with their per-emoji counts and the paid and custom ones identified, the edited mark, the author's signature, the forward's origin, the message a post replies to, the photo and video links, the poll question on a post that has no text at all, and the unfurled link card — plus `collected_at`, so a view count you compare next week is comparable with something.

💸 **You pay for delivered posts, and for nothing else.** A channel that gives nothing — a dead handle, a bot, a group, a channel with no posts in your window — is named in the run's closing line with the reason why, and costs you nothing at all. Neither does a post the dataset refused.

![How Telegram Channel Messages works: a list of channels in, one row per public post out, with views, reactions and media on every row](https://api.apify.com/v2/key-value-stores/RPT1LNaNbQoVImUE9/records/telegram-channel-messages-how-it-works-v1.png)

#### Copy to your AI assistant

```
feedminer/telegram-channel-messages on Apify. Collects the public posts of Telegram channels anonymously — no account, no phone number, no bot token, no cookies. It reads the public message preview t.me/s/<channel> and pages it backwards with ?before=<id> until one of your two bounds is reached. One dataset row per post (channel, channel_title, id, url, published_at, collected_at, edited, text, views, reactions_total, reactions[{emoji, emoji_id, count, kind}], forwarded_from, forwarded_from_url, reply_to_url, author, media, media_urls, poll_question, poll_votes, link_preview_url, link_preview_site, link_preview_title, links). Four measured limits to respect: views are Telegram's own rounded number (a post printed "5.14M" ships as 5140000 and there is no exact count anywhere on this surface), and reaction counts are rounded the same way; a post keeps gathering views for about ten days, which is why every row carries collected_at; a custom or paid reaction prints no character, so emoji is empty and emoji_id identifies it; media_urls holds links for photos and videos only, a video's link is signed and short-lived, and nothing is downloaded. A channel that yields no post is not a row — it is named in the run's closing line with one of six reasons (no_such_handle, not_a_channel, no_preview, no_posts, no_posts_in_window, unreadable) and costs nothing. Call ApifyClient("TOKEN").actor("feedminer/telegram-channel-messages").call(run_input={"channels": ["durov", "bbcrussian"], "maxPostsPerChannel": 200, "publishedAfter": "2026-08-01"}), then client.dataset(run["defaultDatasetId"]).list_items().items. Required: channels (string[], at least one, with or without @, as a t.me link, a t.me/s/ preview URL or a link to a single post, any case, duplicates fold). Optional: maxPostsPerChannel (integer 1-10000, default 100), publishedAfter (date as 2026-08-01 or full RFC3339, read as UTC), debugMode (bool, default false). For one enriched row per channel instead — exact subscriber count, cadence, median reach, reach velocity — use feedminer/telegram-channel-enrichment. Full spec: GET https://api.apify.com/v2/acts/feedminer~telegram-channel-messages/builds/default (Bearer TOKEN) → inputSchema, actorDefinition.storages.dataset, readme. Token: https://console.apify.com/account/integrations
```

***

### How to scrape Telegram channel posts

Paste the channels you want the posts of. That is the whole input.

##### The smallest run

```json
{ "channels": ["durov", "bbcrussian", "tproger"] }
```

A hundred posts from each, newest first.

##### However the channels were sent to you

```json
{ "channels": ["@bbcrussian", "https://t.me/tproger", "t.me/s/durov", "https://t.me/durov/548", "Bloomberg"] }
```

With or without the `@`, as a `t.me` link, as a pasted `t.me/s/` preview URL, as a link to a single post, in any case. Duplicates fold, so a column copied out of a spreadsheet runs unedited.

##### Everything since a date

```json
{
  "channels": ["meduzalive", "bbcrussian"],
  "publishedAfter": "2026-09-01",
  "maxPostsPerChannel": 2000
}
```

Each channel stops at the first post published before your date. Raise the cap when the window is wide and the channels are busy — a news channel publishes hundreds of posts a week, and the cap is what stops before the date does.

##### A deep archive pull

```json
{ "channels": ["durov"], "maxPostsPerChannel": 10000 }
```

Telegram serves twenty posts a page, so this is up to five hundred pages of one channel. A channel shorter than that simply ends: the run stops where the history does.

Measured: 10 000 posts from `bbcrussian` — the deepest single-channel pull the input allows — took **543 pages and under seven minutes**, on each of two runs, well inside the hour a run is given by default. One channel is walked by one worker, so depth on a single channel is paced by that worker alone; a list of channels is walked eight at a time.

### Input parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `channels` | string\[] | — | **Required.** The channel usernames. With or without `@`, as a `t.me` link, a `t.me/s/` preview URL or a link to one post, in any case. Duplicates fold. |
| `maxPostsPerChannel` | integer | `100` | How many posts to take from each channel, newest first. 1 to 10 000. |
| `publishedAfter` | string | empty | Optional date floor, as `2026-08-01`. Read as UTC midnight; write the full RFC3339 form with an offset if the boundary matters in your timezone. |
| `debugMode` | boolean | `false` | Verbose log lines for support. Leave off. |

The two bounds work together and the first one reached stops a channel: whichever of "enough posts" and "far enough back" comes first. A channel also stops on its own when its history ends.

### What data can you extract from a Telegram channel?

**The post** — `id`, `url`, `published_at`, `edited`, `text`, `author` (the per-post signature, when the channel signs its posts).

**Its reach** — `views`, `reactions_total`, and `reactions`, one entry per emoji with `emoji`, `emoji_id`, `count` and `kind` (`standard`, `custom` or `paid`).

**Where it came from** — `forwarded_from` and `forwarded_from_url` when the post was carried over from another channel, `reply_to_url` when it answers another post.

**What it is made of** — `media` (the heaviest attachment: `photo`, `video`, `document`, `voice`, `poll`, `sticker`, `link` or `none`), `media_urls` (the addresses Telegram prints for photos and videos, several of them for an album), `poll_question` and `poll_votes`, the unfurled card's `link_preview_url` / `link_preview_site` / `link_preview_title`, and `links` — every URL the post's own words point at.

**Its context** — `channel`, `channel_title`, and `collected_at`, the moment this run read the page.

A column a post does not carry ships as its zero: an empty string, `0`, or an empty list. There is no `null` anywhere in the row.

![What one row of Telegram Channel Messages carries, field by field: the two dates, the rounded reach, the per-emoji reactions and the post's own URLs](https://api.apify.com/v2/key-value-stores/RPT1LNaNbQoVImUE9/records/telegram-channel-messages-row-anatomy-v1.png)

##### Example row

```json
{
  "channel": "durov",
  "channel_title": "Pavel Durov",
  "id": 539,
  "url": "https://t.me/durov/539",
  "published_at": "2026-08-08T21:11:14Z",
  "collected_at": "2026-09-23T12:17:20Z",
  "edited": false,
  "text": "🏆    The International Olympiad in  AI  finished yesterday in Astana. …",
  "views": 3330000,
  "reactions_total": 69680,
  "reactions": [
    { "emoji": "", "emoji_id": "", "count": 8250, "kind": "paid" },
    { "emoji": "", "emoji_id": "5399847211989246390", "count": 41300, "kind": "custom" },
    { "emoji": "", "emoji_id": "5373223594484587136", "count": 11100, "kind": "custom" },
    { "emoji": "", "emoji_id": "5936157098181135162", "count": 9030, "kind": "custom" }
  ],
  "forwarded_from": "",
  "forwarded_from_url": "",
  "reply_to_url": "",
  "author": "Pavel Durov",
  "media": "link",
  "media_urls": [],
  "poll_question": "",
  "poll_votes": 0,
  "link_preview_url": "https://t.me/durov/537",
  "link_preview_site": "Telegram",
  "link_preview_title": "Pavel Durov",
  "links": [
    "https://ioai-official.org/republic-of-kazakhstan/",
    "https://Cpfed.kz/",
    "https://t.me/durov/537"
  ]
}
```

#### Why `views` is a rounded number, and why every row is dated twice

Telegram prints view counts abbreviated — `5.14M`, `40.7K` — and that is the only figure this surface carries, for anyone. `views` is that string read back into an integer, so a post printed `5.14M` ships as `5140000` and the true count is somewhere in a 10 000-wide band. Reaction counts are printed the same way, and `reactions_total` is our sum of them.

A Telegram post also keeps gathering views for about ten days after it is published. A reach figure with no read time is therefore a number you cannot compare with anything — including the same post read again next week — which is why every row carries `collected_at` beside `published_at`.

#### What a channel that gives nothing tells you

A row is a post, so a channel that has none produces no row and costs nothing. It is named in the run's closing line with one word:

| Reason | What to do about it |
|---|---|
| `no_such_handle` | nobody holds this username — purge it from your list |
| `not_a_channel` | it is a group, a bot or a person; none of those has a public message page |
| `no_preview` | it is a channel, and Telegram serves no public message page for it |
| `no_posts` | the channel has published nothing |
| `no_posts_in_window` | it has posts, all of them older than your `publishedAfter` — widen it |
| `unreadable` | Telegram would not serve the page, from two different exits. Run it again |

### What this Actor covers

![What Telegram Channel Messages reaches, and what it does not](https://api.apify.com/v2/key-value-stores/RPT1LNaNbQoVImUE9/records/telegram-channel-messages-coverage-v1.png)

It reads what `t.me/s/<channel>` publishes to anyone: the posts of a public channel, as far back as you ask.

It does not read private or invite-only channels, group chats, comments and discussion threads, or members — none of those appear on that page. It does not search for channels: you name them. It does not download media; `media_urls` gives you the links.

**For one enriched row per channel instead of one per post** — the exact subscriber count, days since the last post, posts per week, median reach, reach per subscriber, script, content mix and the settled reach velocity — use [Telegram Channel Enrichment](https://apify.com/feedminer/telegram-channel-enrichment). The two are built to be run together: qualify a list of channels with the first, then pull the posts of the ones worth reading with this one.

### Pricing

| Event | What it is |
|---|---|
| `post` | one delivered post. This is the whole bill. |
| Actor start | the platform's own fee, once per run |

You are charged for what reaches your dataset and for nothing else. A channel that gives no post is not billed, and neither is a post the dataset refused.

##### Against the market

The Telegram Actors on the Store that bill per message spread over two orders of magnitude. On the free plan, 1 000 messages costs between **$0.35 and $30** depending on which one you pick; the ones that price in tiers come down to about **$0.11** on the deepest Apify plan. Several also sell a channel-info row, which is the job of [Telegram Channel Enrichment](https://apify.com/feedminer/telegram-channel-enrichment).

Compare them on two things rather than on the field list. **What the run needs to exist**: everything here comes off the public `t.me/s/` page with no account anywhere in the loop, which is also why the perimeter above is what it is. And **what you are billed for**: a post that reached your dataset, never a handle you sent in, never a page that was read to find nothing.

##### What a real run costs

| Run | What it did | Duration |
|---|---|---|
| 10 channels, 1 200 posts each | 9 704 posts over 561 pages | 2 min 47 s |
| 1 channel, the full 10 000-post cap | 10 000 posts over 543 pages | 4 min 10 s |
| 12 channels, 60 posts each, since January | 403 posts, and four channels named as giving nothing | 3.9 s |

##### Free tier

Apify's free plan comes with monthly credits. A first run of a few hundred posts costs a fraction of a cent of platform usage, so there is nothing to commit before you have seen the output.

### Advanced usage

##### Watch a set of channels every morning

```json
{ "channels": ["bbcrussian", "meduzalive", "varlamov"], "publishedAfter": "2026-09-22", "maxPostsPerChannel": 500 }
```

Move `publishedAfter` forward each day and you pull only what is new. Nothing is kept between runs on our side, so the date is the whole state.

##### Build a corpus for text analysis

```json
{ "channels": ["tproger", "javascript"], "maxPostsPerChannel": 5000 }
```

`text` is the message with markup stripped and entities decoded, and `links` holds the URLs separately, so the corpus does not need cleaning before it is tokenised.

##### Rank a channel's own posts by engagement

Sort the rows of one channel on `reactions_total` or on `views`. Both are rounded by Telegram, so read them as an ordering rather than as figures — and compare posts of similar age, since a post published yesterday has not finished gathering views.

##### Follow what a channel amplifies

Filter on `forwarded_from` being non-empty: that is the list of channels this one carries, with the exact post it took each time in `forwarded_from_url`.

### Integrate Telegram data into your stack

Everything Apify offers applies: scheduled runs, webhooks on finish, and exports to S3, Google Sheets, Airtable or a database. The dataset is readable as JSON, CSV, Excel or XML from the API.

##### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("feedminer/telegram-channel-messages").call(run_input={
    "channels": ["durov", "bbcrussian", "tproger"],
    "maxPostsPerChannel": 200,
    "publishedAfter": "2026-08-01",
})

for post in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(post["published_at"], post["channel"], post["views"], post["text"][:80])
```

##### JavaScript

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('feedminer/telegram-channel-messages').call({
    channels: ['durov', 'bbcrussian', 'tproger'],
    maxPostsPerChannel: 200,
    publishedAfter: '2026-08-01',
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const post of items) {
    console.log(post.published_at, post.channel, post.views, post.text.slice(0, 80));
}
```

### Performance

| Measure | Value | Measured on |
|---|---|---|
| Posts per second | 58 | run `2kxvM6LRqgi4IRBmu`, 9 704 posts in 166.7 s |
| Posts per request | 17.3 | the same run: 561 pages for 9 704 posts |
| Depth reached | 561 pages in one run | the same run, ten channels at once |
| Depth on one channel | the full 10 000-post cap, 543 pages, in 4 min 10 s | run `dn4vQZOxWlmLEKMG1`, `bbcrussian` alone |
| Memory used | 46 MB of the 256 asked for | the stress run, and the single-channel one |
| Requests refused by Telegram | none | every run to date, on every channel |
| Rows kept when a run is stopped early | 8 362 of the 8 431 collected | run `5R7wkh4FJkpvewVYK`, stopped 65 s in |

Posts are written to the dataset as each page is read, which is why a run that is stopped or times out still hands you what it had already written. The gap is what was in flight at the moment it stopped — 69 posts in the run above — and you are not charged for those: billing follows what the dataset accepted.

### FAQ

**Do I need a Telegram account, a phone number or a bot token?**
No. There is nothing to connect and nothing to keep alive on your side. Every read is the public `t.me/s/<channel>` page, served to anyone who asks.

**How far back can it go?**
As far as the channel's history, up to the cap of 10 000 posts per channel the input allows. Telegram serves twenty posts a page and this Actor pages backwards until your cap, your date, or the end of the channel — whichever comes first. The full cap has been walked on one channel: `bbcrussian`, 10 000 posts over 543 pages in under seven minutes, nothing refused. A single channel is read by one worker, so that is the pace depth runs at — and it fits the hour a run is given by default.

**Why are the view counts round?**
Because Telegram publishes them that way. The page prints `5.14M`, not `5 143 882`, and there is no exact figure anywhere on this surface. The same is true of reaction counts.

**Why do some reactions have no emoji?**
Because Telegram does not print a character for them. A custom reaction is an identifier — it is in `emoji_id` — and a paid reaction is Telegram Stars. `kind` tells you which of the three you are looking at.

**A channel I know is alive came back as giving nothing.**
Check the reason in the run's closing line. `no_preview` means Telegram genuinely serves no public message page for it. `unreadable` means the page would not load from two different exits — run it again. There is also a case this page will not claim to know about: Telegram hides some channels from some countries, and this Actor does not pin its exit.

**Is the data live?**
Yes. Every run reads Telegram at that moment. Nothing is cached, nothing is resold, and nothing is kept between runs — which is also why `collected_at` is on every row.

**What does it not do?**
It does not find channels for you, does not read private or invite-only channels, does not read group chats, comments or discussion threads, does not list members, and does not download media files.

***

### Support

- Found something off in the output, or need a field that is missing? Open the **Issues** tab on this Actor's page with the input you ran and what you expected. Issues are read daily.
- Need a custom variant — a different bound, a filter, a column this one does not carry? Ask in the Issues tab; FeedMiner builds on request.

### Legal compliance

This Actor collects only public data, the same information any visitor can see on `t.me`. Message text, signatures and channel titles can contain personal data: processing information about people in the EU, the UK or California requires a lawful basis under GDPR or CCPA. Use the data for market research, analytics and business intelligence, not for spam or harassment.

***

**FeedMiner** · Fast, honestly priced scrapers, kept alive when platforms change.

Telegram Channel Messages · [Telegram Channel Enrichment](https://apify.com/feedminer/telegram-channel-enrichment) · [All FeedMiner Actors](https://apify.com/feedminer)

# Actor input Schema

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

The channel usernames you want the posts of. With or without @, as a t.me link, a pasted t.me/s/ preview URL or a link to a single post, in any case — duplicates fold. A handle that turns out to be a group, a bot, a person or nobody yields no rows and is named in the run's closing line with the reason.

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

How many posts to take from each channel, newest first. Telegram serves twenty per page, so this is what decides how deep the run pages back — and what it costs.

## `publishedAfter` (type: `string`):

Optional date floor, as 2026-08-01. Posts published before it are not collected, and a channel stops as soon as it reaches one. Read as UTC midnight; write the full form with an offset (2026-08-01T00:00:00+02:00) if the boundary matters in your own timezone. Leave empty to be bounded by the per-channel count alone.

## `debugMode` (type: `boolean`):

Verbose log lines for support. Leave off.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "bbcrussian",
    "tproger"
  ],
  "maxPostsPerChannel": 100,
  "debugMode": false
}
```

# Actor output Schema

## `posts` (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": [
        "durov",
        "bbcrussian",
        "tproger"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("feedminer/telegram-channel-messages").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",
        "bbcrussian",
        "tproger",
    ] }

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,feedminer/telegram-channel-messages"
        }
    }
}
```

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/by4HfdjpgOAUprgoQ/builds/BqVSVCNqByIPiQXtj/openapi.json
