# Telegram Channel Scraper & Monitor — Posts, Views, No Login (`rezaba002/telegram-channel-reader`) Actor

Scrape public Telegram channel posts without login, bot token or API key. One clean row per post: text, views, reactions, links, hashtags, forwards and detected language, with Persian and Arabic text normalized. Monitor mode returns only new posts. Export JSON, CSV or Excel.

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

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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 Reader

Read public Telegram channels as clean, agent-ready data. You give it channel usernames or `t.me` links. It returns one row per post with:

- the original text and a normalized version
- detected language, with a confidence score
- links, hashtags and mentions
- views and reactions
- media type, poll questions and options
- forward source and reply target
- duplicate detection across the channels in a run

Monitoring mode returns only the posts that are new since your last run.

Built for AI agents and for people who track news across languages. Text handling is tuned for **Russian, Ukrainian, Arabic, Persian, Turkish and English**, and language detection covers 176 languages.

### What you get

- **Agent-ready rows.** Every post row has the same flat set of fields, with `null` where something does not apply.
- **Clean text.** `text_normalized` has invisible characters and right-to-left control marks removed. Persian and Arabic letter variants (ي/ی, ك/ک) and digits (۱۲۳ / ١٢٣ → 123) are unified, and Persian half-spaces (ZWNJ) are kept where they belong. The original `text` is always kept as well.
- **Language detection** per post, with a confidence score.
- **Monitoring mode** (`onlyNew`): a checkpoint is kept per channel, so the next run returns only newer posts.
- **Clear failures.** A missing, private or empty channel returns one `error` row with a reason and is never charged.

Media files are not downloaded. The `media_type` field tells you what each post contains.

### Input

| Field | Default | What it does |
|---|---|---|
| `channels` | required | Usernames or links: `durov`, `@durov`, `https://t.me/durov`, `https://t.me/s/durov`, `https://t.me/durov/123` |
| `maxMessagesPerChannel` | 50 | Newest posts to return per channel (1–5000) |
| `onlyNew` | false | Return only posts newer than the previous run with the same `stateName` |
| `stateName` | `default` | Name of the checkpoint set, so separate monitors don't interfere |
| `newerThan` | – | Skip older posts: `2026-10-01`, an ISO date-time, or `3 days` / `12 hours` |
| `proxyConfiguration` | off | Optional proxy for fetching `t.me` |

#### Example input

```json
{
  "channels": ["durov", "https://t.me/bbcpersian", "@meduzalive"],
  "maxMessagesPerChannel": 20,
  "onlyNew": true,
  "stateName": "morning-brief"
}
```

### Output

Every row has a `row_type`:

| `row_type` | Meaning | Charged |
|---|---|---|
| `message` | One post | free during early access |
| `error` | Bad input, unavailable or empty channel, fetch failure | no |
| `notice` | Expected outcome, e.g. `no_new_messages` in monitoring mode | no |

Posts come out oldest first within each channel.

#### Example message row (real output, text shortened)

```json
{
  "row_type": "message",
  "channel": "durov",
  "channel_title": "Pavel Durov",
  "message_id": 548,
  "url": "https://t.me/durov/548",
  "date": "2026-09-11T16:04:02+00:00",
  "text": "🤝 Telegram has become the sponsor of Codeforces — the largest competitive programming platform in the world. …",
  "text_normalized": "🤝 Telegram has become the sponsor of Codeforces — the largest competitive programming platform in the world. …",
  "language": "en",
  "language_confidence": 0.9822,
  "translation": null,
  "translation_language": null,
  "views": 2120000,
  "reactions": [{"emoji": "paid_star", "count": 21400}, {"emoji": "custom:5399847211989246390", "count": 32500}],
  "reactions_total": 72300,
  "media_type": null,
  "media_count": 0,
  "links": ["https://codeforces.com/blog/entry/156620"],
  "hashtags": [],
  "mentions": [],
  "forwarded_from_name": null,
  "forwarded_from_channel": null,
  "forwarded_from_url": null,
  "forwarded_from_author": null,
  "reply_to_message_id": null,
  "link_preview_url": "https://codeforces.com/blog/entry/156620",
  "link_preview_title": "Telegram Returns as Title Sponsor of Codeforces!",
  "poll_question": null,
  "poll_options": [],
  "poll_voters": null,
  "author_signature": "Pavel Durov",
  "edited": false,
  "is_service": false,
  "duplicate_of": null,
  "scraped_at": "2026-10-07T12:25:49+00:00"
}
```

`translation` and `translation_language` are reserved for the planned translation add-on and are always `null` for now.

#### Example error row

```json
{
  "row_type": "error",
  "channel": "thischanneldoesnotexist_zz9q",
  "input": "thischanneldoesnotexist_zz9q",
  "error_code": "channel_unavailable",
  "error_message": "@thischanneldoesnotexist_zz9q has no public web preview: it does not exist, is private, is a user or bot rather than a channel, or has the preview turned off.",
  "scraped_at": "2026-10-07T12:25:49+00:00"
}
```

Error codes: `invalid_input`, `invalid_channel`, `channel_unavailable`, `channel_empty`, `fetch_failed`. Notice codes: `no_new_messages`, `translation_unavailable`.

### Pricing

Free during early access: you only pay Apify's usual platform usage for your own runs. Pay-per-event pricing (per returned post) may be introduced later. Error and notice rows will always be free.

### Limits, stated plainly

- **Public channels with a web preview only.** The data comes from `https://t.me/s/<channel>`, the same page anyone sees without logging in. Private channels, groups, users, bots and channels whose owners turned off the preview return `channel_unavailable`.
- **Views are rounded** the way Telegram shows them (`18.9M` → 18900000). Telegram's web preview does not show **forward counts**, so there is no such field.
- An **album** is one row (`media_type: "album"`, `media_count` = number of items), identified by its first message id.
- In monitoring mode, if more than `maxMessagesPerChannel` new posts arrived since the last run, only the newest ones are returned. Raise the limit or run more often.
- **No translation yet.** Posts come back in their original language. Translation is planned as an add-on once it is reliable on fresh news.

### Privacy

Only content that channels publish openly is read. No logins, no member lists, no private groups, and nothing beyond what the public channel page shows.

# Actor input Schema

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

Public Telegram channels to read. Accepts usernames (durov, @durov) or links (https://t.me/durov, https://t.me/s/durov, https://t.me/durov/123). Private channels, groups, users and invite links are not supported and return an error row.

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

Newest posts to return per channel. Keep it small for fast calls.

## `onlyNew` (type: `boolean`):

Return only posts newer than the newest post returned by the previous run with the same stateName. The first run returns the latest posts and saves a checkpoint per channel. If more than maxMessagesPerChannel new posts arrived since the last run, only the newest ones are returned.

## `stateName` (type: `string`):

Name of the checkpoint set used by onlyNew. Use different names to run independent monitors over the same channels. Letters, digits and \_ . - only.

## `newerThan` (type: `string`):

Optional. Skip posts older than this. A date (2026-10-01), a date-time in UTC, or a relative period such as '3 days' or '12 hours'.

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

Optional proxy for fetching t.me pages. Usually not needed.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "bbcpersian"
  ],
  "maxMessagesPerChannel": 50,
  "onlyNew": false,
  "stateName": "default",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

All rows of the run. Filter on row_type: 'message' rows are posts; 'error' and 'notice' rows carry error_code and error_message. Field meanings are in the dataset schema.

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

JSON with per-channel results (number of posts, or 'unavailable' / 'no_messages'), the total number of posts, and whether the run stopped at the charge limit.

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

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

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

```

## MCP server setup

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

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/mFu7g4f723qfD0wKc/builds/gdahw3ahIYgfmP3Hj/openapi.json
