# Telegram Channel Posts Scraper (`marknetic/telegram-channels`) Actor

Posts from public Telegram channels, with rounded counters labelled as approximate and every missing field explained.

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

## Pricing

from $0.35 / 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.
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

## Telegram Channel Scraper — Public Posts, Views & Reactions

Extract posts from public [Telegram](https://telegram.org) channels: text, date, views, reactions, photos, videos, documents, forwards, author signatures and link previews, as structured JSON, CSV or Excel.

No Telegram account, phone number, API key or bot token. It reads the same public web preview anyone can open at `t.me/s/<channel>`.

Built for media monitoring, brand and news tracking, research archives, and keeping an eye on many channels on a schedule.

***

### Three guarantees

**1. You pay per post returned. Nothing else.**

No charge to start a run. No charge for a channel that does not exist, is private, or cannot be read. If a run returns nothing, it costs you nothing, and the run summary says so in plain words.

That matters most for scheduled monitoring, where a run every hour may return only a handful of new posts: you pay for those posts, not for the run.

**2. Every field matches the page, or it is `null` with a reason.**

Telegram's web preview shows view and reaction counts **rounded** ("1.68M"). This actor gives you that figure exactly as shown, plus a number parsed from it in a field whose name ends in `_approx`. A rounded count is never passed off as an exact one.

When a value is missing, every row still has every key, and a `_provenance` block records where each value came from, or why it is `null`:

- `absent`: the post has no such thing (no caption, no signature).
- `undisclosed`: Telegram shows the post but withholds its content from the web ("Please open Telegram to view this post").
- `unparseable`: the value is on the page but could not be read.

**3. Nothing is dropped silently.**

Every channel you ask for is accounted for. One that cannot be read appears in the run summary with the reason: not a public channel, a reserved name, a preview that is unavailable from where the request was made, or a page that could not be fetched. One bad channel does not stop the others.

***

### What you get

One row per post:

| Field | Example |
|---|---|
| `channel` | `durov` |
| `post_id` | `536` |
| `url` | `https://t.me/durov/536` |
| `published_at` | `2026-07-30T16:55:01+00:00` |
| `edited` | `false` |
| `text` | the post text, line breaks kept |
| `views_display` / `views_approx` | `"6.25M"` / `6250000` |
| `reactions` | emoji (or custom-emoji id), paid flag, count as shown and approximate |
| `photo_urls` | direct image URLs |
| `video_count` | videos in the post |
| `document_titles` | titles of attached files and audio |
| `forwarded`, `forwarded_from_name`, `forwarded_from_url` | the forward source, when Telegram shows it |
| `author_signature` | the signature on signed posts |
| `link_preview_url` | the link behind a preview card |
| `preview_withheld` | `true` when Telegram shows "Please open Telegram to view this post" instead of the content |
| `source_url` | the page the post was read from |
| `_provenance` | where each field came from, or why it is `null` |

#### Real sample output

From a run on the Apify platform, with the photo URL and reaction list shortened:

```json
{
  "channel": "durov",
  "post_id": 536,
  "url": "https://t.me/durov/536",
  "published_at": "2026-07-30T16:55:01+00:00",
  "edited": false,
  "text": "🇦🇫😈",
  "views_display": "6.25M",
  "views_approx": 6250000,
  "reactions": [
    { "emoji": null, "emoji_id": null, "paid": true, "count_display": "49.2K", "count_approx": 49200 },
    { "emoji": null, "emoji_id": "5373223594484587136", "paid": false, "count_display": "274K", "count_approx": 274000 }
  ],
  "photo_urls": ["https://cdn4.telesco.pe/file/sA6LMASu6MckEQiLzrDe9dwVzq7EgV2…"],
  "video_count": 0,
  "document_titles": [],
  "forwarded": false,
  "forwarded_from_name": null,
  "forwarded_from_url": null,
  "author_signature": "Pavel Durov",
  "link_preview_url": null,
  "preview_withheld": false,
  "source_url": "https://t.me/s/durov",
  "_provenance": {
    "views_display":    { "source": "dom", "key": ".tgme_widget_message_views",       "reason": null,     "conflict": null },
    "author_signature": { "source": "dom", "key": ".tgme_widget_message_from_author", "reason": null,     "conflict": null },
    "link_preview_url": { "source": null,  "key": null,                              "reason": "absent", "conflict": null }
  }
}
```

The `_provenance` block above is an excerpt; every row carries an entry for every field.

#### A post Telegram will not show on the web

Some posts appear in the channel, with a date and a view count, but the web preview replaces their content with *"Please open Telegram to view this post"*. You still get the row, and it says what happened:

```json
{
  "channel": "bbcrussian",
  "post_id": 100853,
  "url": "https://t.me/bbcrussian/100853",
  "published_at": "2026-10-28T14:10:51+00:00",
  "text": null,
  "views_display": "44.8K",
  "views_approx": 44800,
  "preview_withheld": true,
  "_provenance": {
    "text": { "source": null, "key": null, "reason": "undisclosed", "conflict": null }
  }
}
```

The text is `null` with reason **`undisclosed`**, not `absent`: the post is not empty, Telegram is withholding it from the web. A row with no text and a row whose text was withheld are different facts, and you can tell them apart.

***

### Speed and reliability

Measured on the Apify platform at 1 GB of memory, not estimated:

| | |
|---|---|
| **6,000 posts** from two channels | **60 seconds**, 338 MB peak memory |
| **800 posts** from four channels | **15 seconds**, 211 MB peak memory |
| Pages blocked or rate-limited | **none** in any run |

One request returns about 20 posts with every field, and older pages are fetched in parallel. No browser is launched, which is why runs are fast and light.

Before the first release, 1,200 consecutive page requests through Apify's datacenter proxy all succeeded, with no slowdown from the first request to the last. Stated conservatively, that supports **at least a 99.7% page success rate, with 95% confidence**. It is a measurement, not a promise about every future run.

The newest 200 posts of a channel returned by this actor were checked one for one against a page-by-page walk of the same channel: none missing, none duplicated.

***

### Input

| Field | Description |
|---|---|
| **Channels** | Usernames or links: `durov`, `@durov`, `https://t.me/durov` and `https://t.me/s/durov` all work. Duplicates are removed. |
| **Maximum posts per channel** | The newest N posts of each channel. Defaults to **200**. |
| **Oldest date** | Optional. Only posts published on or after this date; fetching stops once it is passed. |
| **Search query** | Optional. Return only posts matching a word or phrase inside each channel, using Telegram's own channel search. |
| **Concurrency** | Pages fetched at once, 1–30. Defaults to 10. |

```json
{
  "channels": ["durov", "https://t.me/s/telegram"],
  "maxPostsPerChannel": 200,
  "oldestDate": "2026-10-01"
}
```

**Monitoring on a schedule:** set **Oldest date** to a recent date, or keep **Maximum posts per channel** small, and run it every hour or day. There is no start fee, so a run that finds 5 new posts costs 5 posts.

#### What cannot be scraped, and how you are told

| The channel is… | You get |
|---|---|
| private, or an invite link (`t.me/+…`, `joinchat`) | refused before the run fetches anything, with the reason |
| not a channel (a person's account, or no such name) | `not_found` in the run summary |
| a reserved name Telegram redirects to its home page | `not_found` in the run summary |
| showing no web preview from the region the request came from | `not_found`, worded "may be restricted in this region". Requests leave from the US. |
| empty | a note in the log; not counted as a failure |

None of these is charged.

***

### Pricing

**$0.50 per 1,000 posts**, charged as posts are delivered. Nothing on start, nothing for failures. Your Apify plan's discount applies automatically.

At the default of 200 posts per channel, a channel costs about **$0.10**. The maximum cost you set for a run is respected exactly: the run checks it before fetching anything and stops at it, rather than returning posts it cannot charge for.

***

### Typical uses

- **Media and news monitoring**: follow outlets' channels and archive every post with its date and reach.
- **Brand and topic tracking**: search channels for a name or keyword on a schedule.
- **Research datasets**: a channel's history as one table, with rounded counts clearly labelled.
- **Reach comparison**: views and reactions across channels over time.

***

### Limits worth knowing

- **Public channels only.** Groups, private channels and anything behind a login are out of scope; no account is used.
- **Counts are as precise as Telegram makes them.** The web shows views and reactions rounded; exact figures are only available inside the Telegram apps.
- **Comments are not included**, and media files are not downloaded. Photos come as URLs; Telegram's media links may stop working after a while, so download what you need promptly.
- **Telegram's channel search** decides what a search query matches.
- **Requests leave from the US.** A channel that Telegram restricts in the US cannot be read and is reported as such.

***

### Responsible use

This actor reads only what Telegram publishes on its public web preview, at a modest request rate. Posts can contain personal data, and you are responsible for using the results lawfully, including under data-protection law such as the GDPR, and in line with Telegram's terms.

# Actor input Schema

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

Public channel usernames or t.me links.

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

Stop each channel after this many posts. Keep the prefill small: Apify's daily health test must finish inside five minutes.

## `oldestDate` (type: `string`):

Only scrape posts published on or after this date.

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

Optional keyword or phrase to search for within each channel.

## `concurrency` (type: `integer`):

Maximum number of concurrent requests.

## Actor input object example

```json
{
  "channels": [
    "telegram"
  ],
  "maxPostsPerChannel": 50,
  "concurrency": 10
}
```

# Actor output Schema

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

One row per post, with the Overview view selected by default. Switch to All fields to see the \_provenance block.

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

Posts requested and returned, channels that could not be read and why, and what the run was 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": [
        "telegram"
    ],
    "maxPostsPerChannel": 50
};

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

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

```

## MCP server setup

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