# Telegram Channel Scraper — Posts API, Views & Media | $0.50/1k (`glasswing/telegram-channel-scraper`) Actor

Scrape public Telegram channels without login: posts with text, views, photos, videos, files, link previews, reactions and forwards, plus exact subscriber counts. Date range and keyword search. Export JSON, CSV or Excel.

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

## Pricing

from $0.50 / 1,000 results

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

### What does Telegram Channel Scraper do?

Telegram Channel Scraper reads **public Telegram channels** and returns their **posts** (text, views, photos, videos, files, link previews, polls, reactions, forwards, replies, hashtags and links) plus one **channel profile row** per channel (title, description, **exact subscriber count**, photo, verified badge). It works as a simple **Telegram channel API**: give it channel usernames or t.me links and get clean JSON, CSV or Excel back in seconds. No Telegram account, no phone number, no API key, no browser.

Typical use cases:

- 📰 **News and media monitoring**: follow news, crypto, finance or politics channels in any language and pipe every new post into a spreadsheet, a database or an AI summariser.
- 📊 **Channel analytics and benchmarking**: compare subscribers, views, reactions and posting frequency across channels, or track them over time with a schedule.
- 🔎 **Research and archiving**: pull a channel's history for a date range, or search inside channels for a keyword across their whole history.
- 🤖 **AI agents and RAG**: give an agent a list of channels and get structured, self-describing rows it can reason over.

It reads only what Telegram shows logged-out on its public web preview (`t.me/s/<channel>`). It does **not** log in, join groups, or collect member lists, user profiles or phone numbers.

### Why use this Telegram scraper?

- **Fast.** Pure HTTP, about 20 posts per request: 1,000 posts from one channel took 37 seconds and 20 channels × 60 posts took 9 seconds on Apify (real runs, 2026-09-28).
- **Cheap.** $0.50 per 1,000 results, Apify platform usage included.
- **Deep history and date ranges.** Pages back as far as you ask. With **Posted before** it jumps straight to that date by binary search instead of reading every newer page, so "December 2023 on a channel with 400,000 posts" takes about 15 requests.
- **Keyword search inside channels.** Uses Telegram's own in-channel search, so a keyword finds matching posts across the channel's whole history, not just the newest pages.
- **Exact subscriber counts.** The channel row reads the channel's profile card, which shows the exact number (10,608,845), not the rounded header figure (10.6M).
- **Honest results.** Every row carries a `status` (`ok`, `not_found`, `error`). A group, a personal account, a bot or a private invite link comes back as one free `not_found` row that says why. You are billed only for `ok` rows.
- **Works from Apify's datacenter.** In our test runs on Apify (2026-09-28) every one of 428 Telegram page requests succeeded on the first attempt, across 23 public channels in Russian, Arabic, Ukrainian and English, with no proxy.

### What data can Telegram Channel Scraper extract?

Two row types share one dataset; `type` tells them apart.

**Post rows** (`type` = `post`)

| Field | Type | Description |
|---|---|---|
| `channel` | string | Channel username without @ (the channel's primary username) |
| `channelTitle` | string | Channel display name |
| `postId` | integer | Post number in the channel; newer posts have higher numbers |
| `url` | string | Link to the post, `https://t.me/<channel>/<postId>` |
| `postedAt` | string | Publication time, ISO 8601 UTC |
| `text` | string | Post text or caption as plain text (line breaks and emoji kept) |
| `textHtml` | string | Optional (`includeTextHtml`): the text with bold, italic, code, quote, spoiler and link formatting |
| `views` | integer | View count as Telegram shows it ("18.9M" becomes 18900000) |
| `reactions` | array | `{ emoji, count }` per reaction; custom emoji carry `customEmojiId`, paid Star reactions `paid: true` |
| `reactionsCount` | integer | Sum of all reactions |
| `photos` | array | Photo URLs (albums give several) |
| `videos` | array | `{ thumbnailUrl, duration, durationSeconds, videoUrl }` (`videoUrl` only for small videos Telegram serves in the preview) |
| `documents` | array | Attached files and audio: `{ name, details, kind }` |
| `linkPreview` | object | The link card: `{ url, siteName, title, description, imageUrl }` |
| `poll` | object | `{ question, type, options: [{ text, percent }] }` |
| `hashtags` | array | Hashtags in the text |
| `mentions` | array | @usernames linked in the text (usually other channels) |
| `links` | array | Web links in the text |
| `isForwarded` | boolean | The post was forwarded from somewhere else |
| `forwardedFrom` | object | `{ channel, title, postUrl }` when the source is a public channel post |
| `replyToPostId` | integer | The earlier post in the same channel this post replies to |
| `isEdited` | boolean | Telegram marks the post as edited |
| `searchKeyword` | string | Which of your `keywords` found the post |

**Channel rows** (`type` = `channel`, one per channel when **Add one channel profile row** is on)

| Field | Type | Description |
|---|---|---|
| `channel`, `channelTitle`, `url` | string | Username, display name and `https://t.me/<channel>` |
| `subscribers` | integer | Exact subscriber count |
| `description` | string | Channel description (bio) |
| `photoUrl` | string | Profile photo |
| `verified` | boolean | Telegram's verified badge |
| `photosCount`, `videosCount`, `filesCount`, `linksCount` | integer | Media the channel has posted, as Telegram counts them |

Every row also has `status`, `error` and `scrapedAt`.

#### Result status (tri-state output)

| `status` | Meaning | Billed? |
|---|---|---|
| `ok` | A post or a channel profile was extracted. | Yes |
| `not_found` | The name is not a public channel (a group, a person, a bot, a private invite link, a name nobody owns, or a channel whose preview is switched off), or no post matched your date range or keyword. `error` says which. | No |
| `error` | Telegram could not be read after retries (network error, rate limit or a layout change). `error` says why. | No |

### How to use the Telegram Channel Scraper

1. Open the Actor in Apify Console and click **Try for free**.
2. Put channels into **Telegram channels**, one per line: `durov`, `@bbcrussian` or `https://t.me/s/tass_agency` all work.
3. Choose **Posts per channel** (20 is one page). Optionally set **Posted on or after** / **Posted before** and **Keywords**.
4. Click **Start**. The default input (2 channels, 20 posts each) finishes in about 10 seconds.
5. Open the **Output** tab (views: Overview, Media and links, Channel profiles) or **Export** as JSON, CSV, Excel, XML or HTML.

To automate it, use the **API** tab (Node.js, Python, curl) or add a **Schedule**: a daily run with **Posted on or after** = `1 day` gives you each day's new posts.

### How much does it cost to scrape Telegram channels?

This Actor uses **pay-per-event** pricing. Apify platform usage is included in these prices.

| Event | Price |
|---|---|
| Actor start | $0.005 per run |
| Result (`status: ok` row: a post or a channel profile) | $0.0005 per result ($0.50 per 1,000) |

Example: 1,000 posts in one run cost $0.505 ($0.50 for the rows plus $0.005 for the start). The default input (2 channels × 20 posts + 2 channel rows = 42 results) costs about $0.026. Rows with `status` `not_found` or `error` are never billed. Cap spending with **Posts per channel**, **Maximum results** and the run's **Max total charge** option; the Actor stops gracefully when your run's maximum charge is reached.

### Input

See the **Input** tab for the full schema. The main options:

| Field | Type | Default | Description |
|---|---|---|---|
| `startUrls` | array of strings | `["durov", "telegram"]` | Channels: usernames, @names or t.me links |
| `maxPostsPerChannel` | integer | `20` | Newest posts per channel (after filters) |
| `postedAfter` | string | - | Only posts on or after: `YYYY-MM-DD`, ISO time, or `7 days` |
| `postedBefore` | string | - | Only posts before this moment (the Actor jumps there directly) |
| `keywords` | array of strings | - | Search inside each channel with Telegram's own search |
| `includeChannelInfo` | boolean | `true` | Add one channel profile row per channel |
| `includeTextHtml` | boolean | `false` | Add `textHtml` with basic formatting |
| `maxItems` | integer | `1000` | Safety cap on rows for the whole run |
| `proxyConfiguration` | object | off | Not needed; datacenter proxy only if you hit HTTP 429 |

API callers may also send the channel list as `channels`.

Example input (news channels, one week, two keywords):

```json
{
    "startUrls": ["bbcrussian", "@tass_agency", "https://t.me/s/ajanews"],
    "maxPostsPerChannel": 200,
    "postedAfter": "7 days",
    "keywords": ["Путин", "Iran"],
    "includeChannelInfo": true
}
```

### Output

You can download the dataset as JSON, HTML, CSV or Excel. Real rows from a run on Apify (2026-09-28), long URLs shortened with `…`:

```json
[
    {
        "url": "https://t.me/durov",
        "status": "ok",
        "scrapedAt": "2026-09-27T23:43:28.543Z",
        "type": "channel",
        "channel": "durov",
        "channelTitle": "Pavel Durov",
        "description": "Founder of Telegram.",
        "subscribers": 10608845,
        "photoUrl": "https://cdn4.telesco.pe/file/OpC7B9qam6vvIGLzp…Wb0c5RZJMNAvo0zVC0LWKJJ68TA.jpg",
        "verified": true,
        "photosCount": 102,
        "videosCount": 46,
        "linksCount": 200
    },
    {
        "url": "https://t.me/durov/548",
        "status": "ok",
        "scrapedAt": "2026-09-27T23:43:27.034Z",
        "type": "post",
        "channel": "durov",
        "channelTitle": "Pavel Durov",
        "postId": 548,
        "postedAt": "2026-09-11T16:04:02Z",
        "text": "🤝 Telegram has become the sponsor of Codeforces — the largest competitive programming platform in the world.\n\n ⚡CodeForces organizes over 100 coding contests every year and has 4 million contestants signed up.\n\n…",
        "views": 1690000,
        "isForwarded": false,
        "photos": [],
        "videos": [],
        "documents": [],
        "linkPreview": {
            "url": "https://codeforces.com/blog/entry/156620",
            "siteName": "Codeforces",
            "title": "Telegram Returns as Title Sponsor of Codeforces!",
            "description": "Hi, Codeforces!",
            "imageUrl": "https://cdn4.telesco.pe/file/pMiBhAOwyapnw…rnP1Q.jpg"
        },
        "hashtags": [],
        "mentions": [],
        "links": ["https://codeforces.com/blog/entry/156620"],
        "reactions": [
            { "emoji": "⭐", "paid": true, "count": 13600 },
            { "customEmojiId": "5399847211989246390", "count": 28700 },
            { "customEmojiId": "5936157098181135162", "count": 8260 },
            { "customEmojiId": "5373223594484587136", "count": 7760 }
        ],
        "reactionsCount": 58320,
        "isEdited": false
    },
    {
        "url": "https://t.me/rt_russian",
        "status": "not_found",
        "error": "@rt_russian is a channel, but Telegram shows no public web preview for it (the owner switched it off, or it is restricted in the region the request came from). Only public channels whose preview opens at t.me/s/rt_russian are supported.",
        "scrapedAt": "2026-09-27T23:43:28.187Z",
        "type": "channel",
        "channel": "rt_russian"
    }
]
```

A missing field means Telegram did not show it for that post; it is never guessed.

### Tips

- Put many channels in one run instead of one run per channel; you pay the start fee once and channels are read in parallel.
- For monitoring, schedule a run with **Posted on or after** = `1 day` (or `6 hours`) and a generous **Posts per channel**; paging stops as soon as it reaches older posts.
- Media URLs point at Telegram's CDN and are signed; download files you need soon after the run.
- If you read hundreds of channels at once and see HTTP 429 `error` rows, enable Apify Proxy (datacenter) in **Proxy configuration**.

### Limitations

- **Public channels only.** Telegram's web preview shows public channels; public groups, personal accounts, bots and private invite links return a free `not_found` row. Comments under posts and member lists are not shown there and are not collected.
- Some channels switch the preview off or are restricted in some regions; those return `not_found` with that reason.
- A username Telegram does not use for anything public can redirect to telegram.org. That drops the name from the answer, so the `not_found` row says what happened but cannot name the input; the other rows of the run show which input it was.
- View counts and some media counters are rounded the way Telegram displays them (for example 18.9M). The channel's subscriber count is exact.
- Stickers, voice messages, round videos and paid media show only what the preview shows (often a "view in Telegram" placeholder), so such posts may carry just their caption.
- Service messages (channel created, title changed, pinned message) are skipped. Whether a post is currently pinned is not visible in the preview.
- **Keywords** use Telegram's own search, which matches words (with its own stemming), so it can return posts where the word appears in a different form.
- Results reflect the preview at the time of the run; if Telegram changes its layout, rows come back as `error` and the Actor is updated (report it in the **Issues** tab).

### FAQ

#### Do I need a Telegram account, phone number or API key?

No. The Actor reads the public web preview at `t.me/s/<channel>`, the same page anyone can open in a browser without logging in.

#### Can it scrape Telegram groups, members or users?

No. Only public channels' broadcast posts. A group, a user or a bot returns one free `not_found` row saying so. The Actor never collects member lists, user profiles or phone numbers, and it does not output post author signatures; a forward is named only when its source is a public channel.

#### How far back can it go?

As far as the channel's history goes. **Posts per channel** sets how many; **Posted before** jumps straight to a date, and **Posted on or after** stops paging once posts get older than that date.

#### How do I track new posts every day?

Create a **Schedule** with **Posted on or after** = `1 day`. Each run returns only posts from the last day, so you pay only for new posts.

#### Why is a channel `not_found`?

The `error` column explains it: the name is a group, a person or a bot, it is not taken, it is a private invite link, or the channel's owner switched the web preview off (or it is restricted in the region the request came from).

#### Can I use this Actor from an AI agent or MCP client?

Yes. It runs with no input at all (two sample channels), inputs are plain strings, and every row is self-describing (`type`, `status`, `error`).

#### How fast is it?

About 20 posts per request. On Apify, 1,000 posts from one channel took 37 seconds, three channels × 1,000 posts took 82 seconds, and 20 channels × 60 posts took 9 seconds.

### Legal and data-protection notice

This Actor extracts only what Telegram publishes on its public, logged-out channel preview. It does not log in, join chats or get around access controls, and it does not extract private user data such as member lists, user profiles, phone numbers or post author signatures. However, public posts can still contain personal data, for example names mentioned in a news story. Personal data is protected by the GDPR in the European Union and by other regulations around the world. You should not scrape personal data unless you have a legitimate reason to do so. If you are unsure whether your reason is legitimate, consult your lawyers. You are responsible for complying with Telegram's terms of service and applicable law when using the extracted data.

This Actor is an independent tool and is not affiliated with, endorsed by or sponsored by Telegram or its owners. Telegram is a trademark of its respective owner.

# Changelog

This Actor's version history is a separate document: https://apify.com/glasswing/telegram-channel-scraper/changelog.md

# Actor input Schema

## `startUrls` (type: `array`):

Public channels, one per line: a username (`durov`), an @name (`@durov`) or a link (`https://t.me/durov`, `https://t.me/s/durov`, a post link such as `https://t.me/durov/528`). Groups, personal accounts, bots and private invite links are not channels: each returns one free `not_found` row saying why.

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

Same as `startUrls`, for API callers who name the field after what it holds. When given, the example channels in `startUrls` are skipped.

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

Newest posts to return from each channel (after the date range and keyword filters). Telegram's web preview shows about 20 posts per page, so 1,000 posts is about 50 pages and typically well under a minute.

## `postedAfter` (type: `string`):

Only posts published on or after this moment (UTC): `YYYY-MM-DD`, an ISO timestamp, or relative such as `7 days` or `24 hours`. Paging stops as soon as it reaches older posts, so a recent date keeps the run short.

## `postedBefore` (type: `string`):

Only posts published before this moment (UTC): `YYYY-MM-DD`, an ISO timestamp, or relative such as `30 days`. The Actor jumps straight to that point of the channel's history instead of reading every newer page.

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

Optional. Uses Telegram's own search inside each listed channel and returns only matching posts, across the channel's whole history (not just the newest pages). Each keyword is searched separately; a post found by two keywords is returned once. Every post row says which keyword found it in `searchKeyword`.

## `includeChannelInfo` (type: `boolean`):

Adds a row with `type` = `channel`: title, description, exact subscriber count, photo, verified badge and media counters. It costs one extra request and counts as one result per channel.

## `includeTextHtml` (type: `boolean`):

Adds `textHtml`: the post body with its bold, italic, code, quote, spoiler and link formatting kept (a small, safe subset of HTML). Off by default to keep rows small.

## `maxItems` (type: `integer`):

Safety cap on the rows saved by the whole run, across all channels. Each post row and each channel profile row with status `ok` is one billable result; `not_found` and `error` rows are free.

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

Optional. Telegram's web preview answers Apify's datacenter IPs directly, so no proxy is needed. If you read very many channels and see HTTP 429 errors, enable Apify Proxy (datacenter). Residential proxies are not needed.

## Actor input object example

```json
{
  "startUrls": [
    "durov",
    "telegram"
  ],
  "maxPostsPerChannel": 20,
  "includeChannelInfo": true,
  "includeTextHtml": false,
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (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 = {
    "startUrls": [
        "durov",
        "telegram"
    ],
    "maxPostsPerChannel": 20,
    "includeChannelInfo": true,
    "maxItems": 100,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("glasswing/telegram-channel-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 = {
    "startUrls": [
        "durov",
        "telegram",
    ],
    "maxPostsPerChannel": 20,
    "includeChannelInfo": True,
    "maxItems": 100,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("glasswing/telegram-channel-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 '{
  "startUrls": [
    "durov",
    "telegram"
  ],
  "maxPostsPerChannel": 20,
  "includeChannelInfo": true,
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call glasswing/telegram-channel-scraper --silent --output-dataset

```

## MCP server setup

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