# Telegram Channel Scraper — Posts, Views, Media (`dottti/telegram-channels`) Actor

Public Telegram channel posts with date, text, views, reactions, forwards, replies, media links and channel info. Date range, Arabic-safe keyword filter and a new-posts-only alert mode. No login or API key. Status per channel; channel rows are free.

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

## Pricing

from $0.90 / 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 Channel Scraper: Posts, Views, Media

Get posts from public Telegram channels in one run. Each post comes with its date, full text, views, reactions, links, hashtags, mentions, forward and reply details, and media links. Each channel also gets one free row with its title, description, exact subscriber count and photo.

The actor reads the public web preview of a channel (`t.me/s/<channel>`). This is the same page anyone can open in a browser. You need no Telegram account, no phone number and no API key.

### Who uses it

- **OSINT and research analysts** who archive what public channels post, with dates and post links they can cite.
- **News and crisis monitoring teams** who follow Arabic and English news channels and filter by keyword.
- **Crypto and market watchers** who track announcement channels and want an alert when a new post appears.
- **Marketers** who compare views, reactions and posting rhythm across channels.
- **AI and data pipelines** that need clean post text in JSON, with Arabic and other right-to-left text kept exactly as posted.

### What you can do

- Read one or many channels. Paste usernames (`durov`), `@names` or links (`https://t.me/bbcarabic`, `https://t.me/s/durov`).
- Limit each channel to the newest N posts.
- Pick a date range. When the end date is in the past, the actor jumps straight to it with a few requests.
- Keep only posts with your keywords. You choose whether a post needs any keyword or all of them.
- Turn on alert mode. Later runs then return only posts published since the last run.
- Leave media links on or off. Media type and count are always included.

### Input examples

The newest 50 posts from two channels:

```json
{
  "channels": ["durov", "https://t.me/bbcarabic"],
  "maxPostsPerChannel": 50
}
```

Arabic news about two topics in October 2026:

```json
{
  "channels": ["ajanews", "alarabiya", "bbcarabic"],
  "keywords": ["غزة", "إسرائيل"],
  "keywordMode": "any",
  "dateFrom": "2026-10-01",
  "dateTo": "2026-10-31",
  "maxPostsPerChannel": 500
}
```

Alert mode for a schedule:

```json
{
  "channels": ["insiderpaper", "ajanews"],
  "onlyNew": true,
  "stateName": "news-watch",
  "maxPostsPerChannel": 100
}
```

### Output

Every row has a `type` field.

| type | One row is | Charged |
|---|---|---|
| `post` | One channel post | Yes |
| `channel` | One input channel: its info and its status | No |

#### Post row (real output, 2026-10-21)

```json
{
  "type": "post",
  "channel": "AjaNews",
  "id": 517920,
  "url": "https://t.me/AjaNews/517920",
  "date": "2026-10-21T14:15:31.000Z",
  "text": "عاجل | المتحدث باسم الحكومة العراقية: ننفي وجود مقرات للحوثيين في #العراق ولن نسمح باستخدام أراضينا لتهديد دول الجوار",
  "textLength": 117,
  "hashtags": ["#العراق"],
  "mentions": [],
  "links": [],
  "views": 1320,
  "viewsText": "1.32K",
  "author": null,
  "edited": false,
  "forwardedFrom": null,
  "replyTo": null,
  "mediaType": "none",
  "media": [],
  "mediaCount": 0,
  "linkPreview": null,
  "poll": null,
  "reactions": [],
  "isService": false
}
```

A post with a photo and reactions (real output, some fields left out, media URL shortened):

```json
{
  "type": "post",
  "channel": "insiderpaper",
  "id": 45415,
  "url": "https://t.me/insiderpaper/45415",
  "date": "2026-10-21T13:42:13.000Z",
  "text": "ALERT - Multiple websites worldwide could go dark as critical PHP flaws spark global cyber alert\n\nREAD: https://insiderpaper.com/critical-php-flaws-websites-down/",
  "links": ["https://insiderpaper.com/critical-php-flaws-websites-down/"],
  "views": 2260,
  "viewsText": "2.26K",
  "mediaType": "photo",
  "media": [{ "type": "photo", "url": "https://cdn4.telesco.pe/file/jdJIaVWXKbJd4UNbfy5K5zmLkC4OJOg...", "postUrl": "https://t.me/insiderpaper/45415" }],
  "mediaCount": 1,
  "reactions": [
    { "emoji": "🔥", "count": 22, "countText": "22" },
    { "emoji": "🤡", "count": 13, "countText": "13" }
  ]
}
```

An album of 10 photos is one post row with `mediaType: "album_photo"` and 10 items in `media`. Each item has its own `postUrl`.

Paid star reactions look like `{"emoji": "⭐", "paid": true, "count": 6000}`. Custom emoji reactions have no character on the page, so they carry `customEmojiId` instead of `emoji`.

#### Channel row (real output, free)

```json
{
  "type": "channel",
  "input": "ajanews",
  "status": "ok",
  "username": "AjaNews",
  "url": "https://t.me/AjaNews",
  "title": "قناة الجزيرة",
  "description": "الحساب الرسمي لقناة الجزيرة على تيليغرام",
  "subscribers": 1308166,
  "subscribersExact": true,
  "subscribersText": "1.31M",
  "photoUrl": "https://cdn4.telesco.pe/file/FIcDDmiGCKqhOIl8CqK42p4zQ4Hw4Zd...",
  "verified": true,
  "kind": "channel",
  "counters": { "photos": 508, "videos": 220, "files": 9, "links": 9490 },
  "postsDelivered": 100,
  "pagesFetched": 5,
  "stopReason": "max_posts",
  "error": null,
  "charged": false
}
```

#### Post fields

| Field | Meaning |
|---|---|
| `id`, `url` | Post number in the channel and its public link |
| `date` | Publish time, ISO 8601 in UTC |
| `text` | Full post text. Line breaks are kept. Emoji are kept as characters. |
| `hashtags`, `mentions`, `links` | Pulled from the text. Hashtags work in Arabic and other scripts. |
| `views`, `viewsText` | Telegram rounds large counts on the page ("5.83M"). `views` is that number as an integer; `viewsText` is the original. |
| `author` | The signature shown on the post, when the channel signs posts |
| `edited` | True when the page marks the post as edited |
| `forwardedFrom` | Name, username and post link of the source. `hidden: true` means Telegram shows a forward with no source name. |
| `replyTo` | The post this one replies to: id, link, author and a short text preview |
| `mediaType` | `none`, `photo`, `video`, `gif`, `round_video`, `voice`, `audio`, `document`, `sticker`, `location`, `poll`, `link_preview`, `album_photo`, `album_video`, `album` or `unsupported` |
| `media` | One item per photo, video or file: URL, thumbnail, duration, file name and size when the page shows them |
| `linkPreview` | Site name, title, description and image of a shared link |
| `poll` | Question, options and percentages, when the page shows a poll |
| `reactions` | Each reaction with its count |
| `isService` | Notes like "Channel photo updated". These are skipped unless you turn them on. |

#### Channel status values

A bad channel never stops the run. Every input gets one free `channel` row with a `status`:

- `ok`: posts were read.
- `not_found`: Telegram has no public chat with this username.
- `private`: the input is an invite link (`t.me/+...`). Private chats need a login, so the actor does not read them.
- `no_preview`: the chat exists, but Telegram shows no web preview for it. Public groups are in this case.
- `not_channel`: the username belongs to a user or a bot.
- `invalid_input`: the text is not a Telegram username or link.
- `blocked`: Telegram refused every retry and every proxy tier.
- `cut_by_cost_limit` or `skipped_cost_limit`: your maximum cost for the run was reached.

`stopReason` says why a channel ended: `max_posts`, `reached_from_date`, `reached_last_seen`, `end_of_channel`, `max_pages` or `cost_limit`.

The `RUN_SUMMARY` record in the key-value store lists every channel with its status, pages read and timing. It also lists the requests made, status codes, retries, the proxy tier used and the bytes downloaded per 1,000 posts.

### Keyword filter and Arabic text

Keywords are case-insensitive. The filter searches the post text, the link preview title and description, poll text and file names.

For Arabic, the filter ignores diacritics (harakat) and tatweel. It treats these letters as the same: أ إ آ ا, ى ي, ة ه, ؤ و, ئ ي. Arabic-Indic digits match Western digits. So `افريقيا` finds `أفريقيا`, and `العربية` finds `الْعَرَبِيَّةُ`.

The output text is never changed. Normalizing is only used to compare.

Posts that fail the filter are skipped and free. A rare keyword can mean many pages are read for few matches. `maxPagesPerChannel` (default 50 pages, about 1,000 posts) caps that scan.

### Dates

- `dateFrom` and `dateTo` take `YYYY-MM-DD` (UTC), a full ISO time, or a relative value such as `7 days` or `12 hours`.
- A date-only `dateTo` includes that whole UTC day.
- Newest posts come first. The walk stops at the first post older than `dateFrom`.

### Alerts on a schedule

1. Turn on `onlyNew` and give the list a `stateName`.
2. The first run returns the newest posts as a baseline and saves the last post id per channel.
3. Each later run returns only posts newer than the saved id. These rows have `isNew: true`. Their channel row has `alert: "new_posts"`.
4. New posts come oldest first, so if a cost limit cuts the run, the next run picks up right after the last delivered post.
5. If more than `maxPostsPerChannel` posts arrived between two runs, only the newest that many are returned.

State lives in the named key-value store `telegram-channels-state`, under `STATE-<stateName>`. Use different names for different lists.

To schedule it, open the actor in Apify Console, save your input as a task, then add a schedule to the task. Every 15 minutes or every hour works for news and announcement channels. When nothing is new, the run outputs only the free channel rows and you pay nothing for posts.

### Pricing

You pay per post row: **$0.90 per 1,000 posts** ($0.0009 per post).

- Channel info rows are free.
- Status rows for channels that return nothing are free.
- Posts removed by your keyword or date filters are free.
- Set a maximum cost per run in Apify. The actor stops cleanly at that limit and marks the channels it could not finish.

Platform usage is included in the price.

### Speed and limits

Measured on 2026-10-21 from a home connection, no proxy, 256 MB memory:

| Run | Posts | Requests | Time | Downloaded |
|---|---|---|---|---|
| Empty input (default channel) | 20 | 2 | 1.2 s | 25 KB |
| English, 200 posts | 200 | 13 | 8 s | 269 KB |
| Arabic news, 200 posts | 200 | 11 | 6.5 s | 94 KB |
| Media-heavy, 200 posts | 200 | 13 | 7.7 s | 149 KB |
| News channel, 975 posts | 975 | 51 | 30.6 s | 794 KB |
| 5 channels mixed, 100 each | 400 | 30 | 17.5 s | 384 KB |
| Date range in 2020, 2 channels | 48 | 16 | 11.4 s | 321 KB |

- One preview page holds about 20 posts. The actor waits about 0.6 seconds between requests across all channels, to stay polite. Expect about 30 seconds per 1,000 posts.
- Telegram rounds view, reaction and subscriber counts on the preview page. The exact subscriber count comes from one extra small request per channel. Turn off `exactChannelInfo` to skip it.
- Media URLs point to Telegram's CDN. They are signed and expire after some hours. Download what you need soon after the run.
- Some media shows only "Please open Telegram to view this post" on the web. The post row is still output, with `mediaType: "unsupported"` or a `note` on the media item.
- Some channels deleted most of their history. The preview then ends early and `stopReason` is `end_of_channel`.
- The public preview leaves out comments under posts, member lists and private chats. The actor collects none of these.
- Proxy: requests start with no proxy. If Telegram keeps answering 429 or errors, the actor moves to Apify datacenter proxy, then residential. In our tests no proxy was needed.

### Privacy

The actor reads only what the public web preview shows to any visitor. It never logs in, joins chats, reads member lists or looks up phone numbers. You are responsible for using the data in line with the laws that apply to you, such as GDPR or Saudi PDPL when posts name people.

### Input reference

| Field | Default | What it does |
|---|---|---|
| `channels` | `["telegram"]` | Usernames or links |
| `maxPostsPerChannel` | `20` | Posts kept per channel, after filters |
| `dateFrom`, `dateTo` | empty | Date window |
| `keywords` | empty | Keep only posts with these words |
| `keywordMode` | `any` | `any` or `all` |
| `includeMedia` | `true` | Add media URLs |
| `includeServiceMessages` | `false` | Keep notes like "Channel photo updated" |
| `maxPagesPerChannel` | `50` | Scan cap when filters are on (about 20 posts per page) |
| `exactChannelInfo` | `true` | One extra request for the exact subscriber count |
| `onlyNew` | `false` | Alert mode |
| `stateName` | `default` | Separate memory per alert list |
| `proxyConfiguration` | off | Your own proxy; turns the fallback off |
| `autoProxyFallback` | `true` | No proxy, then datacenter, then residential |
| `allowResidentialFallback` | `true` | Allow the residential step |
| `maxConcurrency` | `2` | Channels read in parallel |

# Actor input Schema

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

Public channel usernames or links. Examples: durov, @telegram, https://t.me/bbcarabic, https://t.me/s/durov. Invite links (t.me/+...) are private and get a free status row.

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

Stops each channel after this many posts that pass your filters. Newest posts come first.

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

Only posts on or after this date. Use YYYY-MM-DD (UTC), an ISO time, or a relative value like '7 days' or '12 hours'. Empty = no lower limit.

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

Only posts on or before this date (a date-only value includes that whole UTC day). The actor jumps to this date instead of reading every newer page.

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

Keep only posts that contain these words or phrases. Case-insensitive. Arabic matching ignores diacritics and tatweel, and treats أ/إ/آ/ا, ى/ي and ة/ه as the same letter. Also searches link previews, polls and file names. Filtered-out posts are not charged.

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

any = a post needs one keyword. all = a post needs every keyword.

## `includeMedia` (type: `boolean`):

Adds photo, video, sticker and link-preview image URLs. Media type, count, durations and file names are always included. These CDN links are signed by Telegram and expire, so download what you need soon after the run.

## `includeServiceMessages` (type: `boolean`):

Service messages are notes like 'Channel photo updated'. Off by default.

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

Safety cap. One page holds about 20 posts. Raise it for long date ranges or rare keywords.

## `exactChannelInfo` (type: `boolean`):

Makes one extra small request per channel to read the exact subscriber count (the preview page rounds it, for example 10.3M).

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

Remembers the last post seen per channel. The first run outputs the newest posts as a baseline. Later runs output only posts published since. Use it on a schedule.

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

Separate names keep separate memory, for example 'crypto-watch' and 'gulf-news'. Stored in the named key-value store 'telegram-channels-state'.

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

Leave off to use the automatic fallback below. If you set a proxy here, it is used for every request and the fallback is off.

## `autoProxyFallback` (type: `boolean`):

Starts without a proxy. If Telegram keeps answering 429 or errors, switches to Apify datacenter proxy, then residential.

## `allowResidentialFallback` (type: `boolean`):

Turn off to never use residential proxy.

## `maxConcurrency` (type: `integer`):

Requests are still spaced out across all channels. Higher is faster and more likely to be rate-limited.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "bbcarabic"
  ],
  "maxPostsPerChannel": 20,
  "keywordMode": "any",
  "includeMedia": true,
  "includeServiceMessages": false,
  "maxPagesPerChannel": 50,
  "exactChannelInfo": true,
  "onlyNew": false,
  "stateName": "default",
  "autoProxyFallback": true,
  "allowResidentialFallback": true,
  "maxConcurrency": 2
}
```

# Actor output Schema

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

All rows. Filter by the type field.

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

Status per channel, pages read, stop reason, requests, bytes per 1,000 posts, retries and proxy tier used.

# 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",
        "bbcarabic"
    ],
    "maxPostsPerChannel": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dottti/telegram-channels").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",
        "bbcarabic",
    ],
    "maxPostsPerChannel": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dottti/telegram-channels").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",
    "bbcarabic"
  ],
  "maxPostsPerChannel": 20
}' |
apify call dottti/telegram-channels --silent --output-dataset

```

## MCP server setup

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

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/JgVdDNteJzdMuMUfG/builds/oaJgLYza8r2MNjQO7/openapi.json
