# Telegram Channel Message Scraper (`praise-most-high/telegram-channel-message-scraper`) Actor

Telegram Channel Scraper that returns message text, views, publish dates, media types, forwards and link previews. 36 typed fields per message. Read a whole channel, search inside it, or turn on onlyNewSinceLastRun so a repeat run bills only for new posts. JSON, CSV, Excel.

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

## Pricing

from $6.00 / 1,000 message returneds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### Changelog

Telegram Channel Message Scraper extracts messages from public Telegram channels, and as of
2026-09-02 it no longer charges you a second time for messages it has already delivered to you.

#### 2026-09-02 — `onlyNewSinceLastRun` now defaults to **`true`**

**What changed.** The `onlyNewSinceLastRun` switch used to be **off** unless you turned it on. It
is now **on** unless you turn it off.

**Why.** With it off, every run read each channel from its newest message backwards, so running
this Actor twice over the same channel delivered the same messages twice — and charged you for
them twice. You were paying again for data you already had. A default should not do that, so it
no longer does.

**What it costs you, on the numbers in the Pricing section.** One channel with 100 messages of
history, polled 8 times in a day, publishing 2 new posts that day:

| | Old default (`false`) | New default (`true`) |
|---|---|---|
| First run | $0.602 | $0.602 |
| The other seven runs | $4.214 — the same 100 messages, seven more times | $0.026 — 7 channel reads plus the 2 new posts |
| **That day** | **$4.816** | **$0.628** |

Same messages delivered. **87% less spent.**

**How to get the old behaviour back.** Set one field:

```json
{
  "channels": ["durov"],
  "onlyNewSinceLastRun": false
}
```

That restores the full re-read on every run, exactly as before. It is a legitimate choice and it
is not going away — a full re-read is what you want if you need messages that were **edited**
after you first collected them, if you want **current view counts** on older posts rather than the
counts at the moment they were first scraped, or if your own pipeline **de-duplicates downstream**
and would rather have everything.

**Nothing else changed.** Same fields, same output records, same prices — `message-returned` is
still $0.006 and `channel-resolved` is still $0.002. Only the starting position of one switch moved.

### What this Actor does

Telegram Channel Message Scraper extracts every message a public Telegram channel has published
and hands you each one as a typed record — the text, the view count, the publish date, the media
attached to it, who it was forwarded from, and the link preview Telegram itself resolved. You give
it a channel username; you get back rows you can sort, filter, diff and export.

![Actor icon](https://apify-image-uploads-prod.s3.us-east-1.amazonaws.com/0ezaunoiVKF5LFUup-actor-RsfpcJ7iWgfmFD3R7-gQBGbIdvOh-telegram-channel-message-scraper.png)

It reads the same public web preview a logged-out visitor sees at `https://t.me/s/<channel>`. No
account, no phone number, no API key, no session string. If a channel is readable in a browser
without signing in, this Actor can read it; if it is not, the Actor says so instead of returning an
empty list that looks like an empty channel.

### If you run this more than once, `onlyNewSinceLastRun` is already doing the right thing

**It is on by default, and it is what stops a repeat run billing you twice for the same message.**

With it off, each run reads every channel from the newest message backwards, so a run you repeat
an hour later returns the same messages again — and charges for them again. Poll a channel eight
times a day and you are billed eight times for the posts that were already delivered on the first
run. That used to be the default. Since 2026-09-02 it is not.

With it on, the Actor records the newest message ID it delivered for each channel in a key-value
store inside your own Apify account, and the next run returns — and bills for — only what has been
published since. Nothing else about the output changes.

```json
{
  "channels": ["durov"],
  "onlyNewSinceLastRun": true
}
```

Worked at the prices in the Pricing section below: one channel with 100 messages of history,
polled eight times in a day, publishing two new posts that day.

| | `onlyNewSinceLastRun: false` | Default (`true`) |
|---|---|---|
| First run | $0.602 — 100 messages + 1 channel read | $0.602 — 100 messages + 1 channel read |
| The other seven runs | $4.214 — the same 100 messages, seven more times | $0.026 — 7 channel reads plus the 2 new posts |
| **That day** | **$4.816** | **$0.628** |

Same messages delivered, 87% less spent.

**When turning it off is the right call.** A full re-read is what you want if you need messages
that were edited after you first collected them, if your own pipeline de-duplicates downstream, or
if you want the current view counts on older posts rather than the counts at the moment they were
first scraped. Those are real reasons, which is why this Actor does not decide for you and does not
change your input behind your back. It just makes sure you know the switch is there.

### Key features

| Feature | Detail |
|---|---|
| Input | Channel usernames, `@usernames` or `https://t.me/<name>` links — mixed freely in one list |
| Fields per message | 36 typed fields, including views, media types, forwards, replies and link previews |
| Search | Runs Telegram's own in-channel search with `searchQuery` and returns only what matched |
| Incremental | `onlyNewSinceLastRun` — **on by default.** A repeat run returns, and bills for, only messages published since the last run. Set it to `false` for a full re-read every time |
| Date and ID floors | `minDate` and `sinceMessageId` stop the walk early, so you neither fetch nor pay for old messages |
| Channel record | One optional summary row per channel: title, description, subscriber count, media counters |
| Unreadable channels | Reported as `unavailable` with a reason, and never charged for |
| Output | Flat rows, one per message — no nested blobs, exports cleanly to CSV and Excel |

### How it works

1. **Normalise every reference.** `durov`, `@durov` and `https://t.me/durov` all resolve to the same
   channel, and duplicates in your list are collapsed before anything is fetched.
2. **Read the public preview.** The Actor requests `https://t.me/s/<channel>`. That page carries the
   most recent messages plus Telegram's own `before=` cursor for the page before it.
3. **Walk backwards.** It follows that cursor page by page until it has reached your
   `maxMessagesPerChannel` cap, crossed your `minDate` or `sinceMessageId` floor, or run out of
   channel. A cursor that stops advancing ends the walk rather than looping.
4. **Charge, then deliver.** Records are charged before they are pushed and delivery is truncated to
   the number actually charged, so the row count you receive and the row count you are billed for
   are the same number.

A single channel at the default cap of 100 messages typically finishes in well under a minute.

#### Two things this Actor deliberately does not do

- **It does not guess.** A field the page does not carry comes back `null`, not `0` and not `""`. A
  message with no view counter and a message with zero views are different facts and stay different.
- **It does not turn a failure into an empty result.** A channel that is private, deleted, or has
  its web preview switched off is written to the run summary as `unavailable` with the reason, is
  excluded from the dataset, and is not billed.

### Input

The minimum input is a single channel name. Everything else has a working default.

```json
{
  "channels": ["durov", "@telegram", "https://t.me/durov"],
  "maxMessagesPerChannel": 100,
  "searchQuery": "",
  "minDate": "2026-01-01",
  "sinceMessageId": 0,
  "onlyNewSinceLastRun": true,
  "includeChannelRecord": true
}
```

| Field | Type | Default | Meaning |
|---|---|---|---|
| `channels` | array of string | — | Public channel usernames, `@usernames` or `t.me` links. Required. |
| `maxMessagesPerChannel` | integer | `100` | Hard cap per channel. Bounds runtime and spend. |
| `searchQuery` | string | — | Runs Telegram's in-channel search; only matches are returned. |
| `minDate` | string | — | ISO date. Messages published before it are neither returned nor charged. |
| `sinceMessageId` | integer | — | Only messages with a higher ID. Every message URL ends in its ID. |
| `onlyNewSinceLastRun` | boolean | `true` | Stores the newest delivered ID per channel and resumes from it, so a repeat run is not charged for messages already delivered. Set `false` for a full re-read every run. |
| `includeChannelRecord` | boolean | `true` | Adds one summary row per channel alongside the messages. |

### Output

Every run writes one row per message to the default dataset. The record below is a real row from a
real run against `t.me/s/telegram`, copied unchanged:

```json
{
  "recordType": "message",
  "channelUsername": "telegram",
  "channelTitle": "Telegram News",
  "messageId": 459,
  "url": "https://t.me/telegram/459",
  "datePublished": "2026-08-26T19:12:24+00:00",
  "text": "Signed Gifts. You can now add your signature and a comment to gifts you buy via the gift marketplace.\n\nAugust Features\n1 • 2 • 3 • 4 • 5 • More",
  "textLength": 143,
  "views": 590000,
  "authorSignature": null,
  "isEdited": false,
  "isForwarded": false,
  "forwardedFrom": null,
  "forwardedFromUrl": null,
  "isReply": false,
  "replyToUrl": null,
  "replyToText": null,
  "mediaTypes": ["video"],
  "photoUrls": [],
  "videoThumbUrl": "https://cdn1.telesco.pe/file/NTKaupuR9Z_bBp0...",
  "videoDurationSeconds": 12,
  "voiceDurationSeconds": null,
  "documentTitle": null,
  "documentExtra": null,
  "pollQuestion": null,
  "pollType": null,
  "pollOptions": [],
  "linkPreviewTitle": null,
  "linkPreviewDescription": null,
  "linkPreviewSiteName": null,
  "linkPreviewUrl": null,
  "linkPreviewImageUrl": null,
  "outboundLinks": [
    "https://t.me/telegram/382",
    "https://t.me/telegram/455",
    "https://t.me/telegram/456"
  ],
  "hashtags": [],
  "mentions": [],
  "scrapedAt": "2026-08-30T03:57:46.459Z"
}
```

Only `videoThumbUrl` and the `outboundLinks` list are abbreviated above — the CDN token is several
hundred characters long and the message links to six sibling posts. Everything else is verbatim.

#### The channel summary record

With `includeChannelRecord` left on, each channel also produces one row of type `channel`:

```json
{
  "recordType": "channel",
  "username": "durov",
  "url": "https://t.me/durov",
  "title": "Pavel Durov",
  "description": "Founder of Telegram.",
  "isVerified": false,
  "subscribers": 11000000,
  "photosCount": 102,
  "videosCount": 45,
  "filesCount": null,
  "linksCount": 197,
  "scrapedAt": "2026-08-30T03:57:32.785Z"
}
```

`filesCount` is `null` there because that channel's page shows no file counter at all. That is the
tri-state the Actor keeps: present-and-zero and not-present are not the same answer.

The dataset exports to JSON, CSV, Excel and XML from the Storage tab, and the same rows are readable
through the dataset API.

### Pricing

This Actor is priced per event, so you pay for what it delivers rather than for how long it runs.

| Event | Price | Charged when |
|---|---|---|
| `message-returned` | $0.006 | Once per message record delivered to the dataset |
| `channel-resolved` | $0.002 | Once per channel whose public preview was successfully read |

There is no per-run start fee and no monthly minimum. A run over one channel returning 100 messages
costs $0.602. A channel that turns out to be unreadable costs nothing. A `sinceMessageId` or
`minDate` filter that matches nothing costs only the $0.002 channel read.

**The setting that moves your bill the most is `onlyNewSinceLastRun`, and it is on by default.**
Left on, a repeat run charges only for what was published since the last one. Turned off, a repeat
run over the same channel returns the same messages and charges for them again — in the worked
example above, $4.816 a day instead of $0.628 for the same delivered messages. If you turn it off,
read the section near the top of this page first so the bill is not a surprise.

**How that price was set.** The two public Telegram message Actors on the Store that charge per
message price it at $0.005 and $0.0025 per record. This Actor sits slightly above both because its
record is wider than a plain text-and-date dump — 36 fields including view counts, forward
provenance, media typing and the resolved link preview. Deliberately, there is room to move the
price down and none is needed to move it up.

Charges are applied before records are handed over, and delivery is truncated to whatever your own
budget cap allowed, so you are never billed for a row you did not receive.

### FAQ

**Do I need a Telegram account, a phone number or an API key?**
No. The Actor reads the public web preview that Telegram serves to logged-out visitors. It holds no
credentials and asks you for none.

**Can it read private channels or group chats?**
No, and it will not pretend to. A private channel, a group chat and a deleted username are all
unreadable from outside, and all three are reported as `unavailable` rather than as empty.

**Why did a public channel come back as unavailable?**
Channel owners can switch the web preview off. When they do, `t.me/s/<name>` serves no messages even
though the channel is public inside the app. There is no way around that from outside, so the Actor
reports it instead of guessing.

**How far back can it go?**
As far as the channel's own preview goes. The walk follows Telegram's `before=` cursor until the
cursor stops, which is the start of the channel. Use `maxMessagesPerChannel` to stop earlier.

**Does it get comments or reactions?**
No. The public preview does not carry the comment thread or the reaction counts, so neither is in
the schema. A field this Actor does not serve is a field it does not advertise.

**Can I run it on a schedule and only get what is new?**
Yes — that is what `onlyNewSinceLastRun` is for, and since 2026-09-02 it is **on by default**, so
you get that behaviour without asking for it. It keeps the newest delivered message ID per channel
in a key-value store inside your own account, resumes from there, and bills you only for what is
new. Set it to `false` if you would rather re-read the whole window every run.

**Are view counts exact?**
They are exactly what the page shows, which Telegram rounds above a thousand. `590000` in a record
means the page said `590K`. The rounding is Telegram's, not this Actor's.

**Do you keep my results?**
The dataset lives in your own Apify account under your own storage. Nothing is copied anywhere else
and nothing is retained by the publisher.

### Limits and troubleshooting

- **`maxMessagesPerChannel` is a cap, not a target.** A channel with fewer messages returns fewer
  rows, and you are charged for the rows you got.
- **A search query narrows before it charges.** `searchQuery` is Telegram's own search, so a term
  that matches nothing produces no message rows and no message charges.
- **Rate limiting.** The Actor retries a `429` or a `5xx` three times with a growing pause. A page
  that stays unreachable ends that channel's walk and is reported, never treated as an empty page.
- **Views can be missing.** Some channels hide the counter. Those records carry `views: null`.
- **The walk is bounded.** It stops after 200 pages per channel even if the cap has not been
  reached, so a pathological cursor cannot spend your budget in a loop.

### Integrations and API

The Actor is callable from anywhere the Apify API reaches.

```bash
curl -X POST "https://api.apify.com/v2/acts/praise-most-high~telegram-channel-message-scraper/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"channels":["telegram"],"maxMessagesPerChannel":50}'
```

- **Scheduling** — run it nightly from the Apify scheduler and, with `onlyNewSinceLastRun` on by
  default, the dataset only ever grows by what was actually posted.
- **Webhooks** — fire on `ACTOR.RUN.SUCCEEDED` to push new messages into your own store the moment
  they land.
- **Python and JavaScript SDKs** — both call it the same way; see the
  [Apify client documentation](https://docs.apify.com/api/client/js/).
- **MCP** — reachable as a tool from any MCP-speaking agent through Apify's own MCP server, with
  `LIMITED_PERMISSIONS` so it can be paid for per call.

### Legal and data handling

**Unofficial.** This Actor is not affiliated with, endorsed by, or sponsored by Telegram Messenger
Inc. or Telegram FZ-LLC. "Telegram" is a trademark of its owner and is used here only to identify
the service being read.

The Actor reads a public, logged-out web page that a channel's owner chose to publish, and it
collects no personal data beyond what that owner put on it, nothing behind an authentication
boundary, and no member lists. It stores no credentials because it needs none.

Whether your own use of the output is permitted is a question about your jurisdiction and your
purpose, and it is yours to answer — this Actor makes no representation about it. Telegram's own
[Terms of Service](https://telegram.org/tos) govern your relationship with Telegram.

### Support and feedback

Open an issue on the Actor's Issues tab. Bug reports that include the run ID and the input that
produced the problem are answered fastest. Feature requests are read, and they are the main source
of what gets built next.

# Actor input Schema

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

Public channel usernames, @usernames or https://t.me/<username> links. Private channels, group chats and channels whose owner has switched the web preview off are reported as unavailable and are never charged for.

## `maxMessagesPerChannel` (type: `integer`):

Hard cap on messages returned per channel. Bounds both runtime and spend. A channel with fewer messages than the cap returns fewer records and costs less.

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

Optional. Runs Telegram's own in-channel search and returns only matching messages. Leave empty to walk the channel in reverse chronological order instead.

## `minDate` (type: `string`):

Optional ISO date such as 2026-01-01. Messages published before it are not returned and not charged. The walk stops as soon as it passes this date.

## `sinceMessageId` (type: `integer`):

Optional. Every message URL ends in its ID, so https://t.me/durov/544 is ID 544. Only messages with a higher ID are returned.

## `onlyNewSinceLastRun` (type: `boolean`):

ON by default since 2026-09-02, so a repeat run never bills you again for messages it has already delivered. It remembers the newest message ID it delivered per channel in a key-value store in your own account, and returns only messages newer than that on the next run. Set it to false to go back to a full re-read every run — the right choice if you need edited messages, refreshed view counts, or you de-duplicate downstream.

## `includeChannelRecord` (type: `boolean`):

Writes one extra record per channel with the channel title, description, subscriber count and media counters. Turn it off if you want messages only.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "telegram"
  ],
  "maxMessagesPerChannel": 100,
  "onlyNewSinceLastRun": true,
  "includeChannelRecord": true
}
```

# Actor output Schema

## `messages` (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",
        "telegram"
    ],
    "maxMessagesPerChannel": 100,
    "searchQuery": "",
    "minDate": "",
    "sinceMessageId": 0,
    "onlyNewSinceLastRun": true,
    "includeChannelRecord": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("praise-most-high/telegram-channel-message-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",
    ],
    "maxMessagesPerChannel": 100,
    "searchQuery": "",
    "minDate": "",
    "sinceMessageId": 0,
    "onlyNewSinceLastRun": True,
    "includeChannelRecord": True,
}

# Run the Actor and wait for it to finish
run = client.actor("praise-most-high/telegram-channel-message-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"
  ],
  "maxMessagesPerChannel": 100,
  "searchQuery": "",
  "minDate": "",
  "sinceMessageId": 0,
  "onlyNewSinceLastRun": true,
  "includeChannelRecord": true
}' |
apify call praise-most-high/telegram-channel-message-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,praise-most-high/telegram-channel-message-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/RsfpcJ7iWgfmFD3R7/builds/vlkPfzb94QPfqcmK8/openapi.json
