# Telegram Channel Scraper: Messages, Views, Media (`datagleaner/telegram-channel-scraper`) Actor

Telegram channel scraper: messages, views, reactions, photos, videos, forwards, replies, polls and channel info from public channels. No login, no API key, no phone number. $0.50 per 1,000 messages.

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

## Pricing

Pay per event

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: messages, views, reactions and media, no login

**Scrape public Telegram channels without a Telegram account: every message with date, text, views, reactions, photos and videos, forwards, replies, polls and links, plus one info row per channel.** Export to JSON, CSV or Excel. No phone number, no API key, no browser.

**Price: $0.50 per 1,000 messages** ($0.0005 per message), and $0.001 per channel info row. 100 messages cost 5 cents.

### Why use this one

- **No login, so nothing to get logged out of.** It reads the public web preview Telegram serves at `t.me/s/<channel>`; no session of yours is used.
- **Dates that stop the run.** `onlyNewerThan` (a date or "7 days") ends paging when messages get older, so you do not pay for history you did not ask for. `searchQuery` uses Telegram's own in-channel search.
- **Failures are not billed.** A channel that is private, missing or has its preview off is reported in the log and skipped, with no charge. Rate limits are waited out with backoff.
- **Rich rows.** Reactions with counts, forwarded-from, reply-to, media list, polls, link previews, hashtags and mentions.
- **Honest limits:** public channels with the web preview only. No groups, no member lists, no comments from linked discussion groups; counts are rounded by Telegram.

### Minimal input

```json
{"channels": ["durov", "telegram"], "maxMessagesPerChannel": 100, "onlyNewerThan": "7 days"}
```

### Sample output row (real platform run, long text shortened)

```json
{
  "type": "message",
  "messageId": 551,
  "channel": "durov",
  "channelTitle": "Pavel Durov",
  "channelSubscribers": 10400000,
  "url": "https://t.me/durov/551",
  "date": "2026-10-10T14:22:43+00:00",
  "isEdited": false,
  "text": "💸 If your wallet doesn’t look like this, don’t even bother sending me money for my  birthday! 🎂",
  "textHtml": "<tg-emoji emoji-id=\"5447458260200214425\"><i class=\"emoji\" style=\"background-image:url('//telegram.org/img/emoj...",
  "views": 689000,
  "reactions": [
    {
      "emoji": "⭐",
      "count": 56800,
      "paid": true
    },
    {
      "emoji": null,
      "count": 27100,
      "customEmojiId": "5936157098181135162"
    },
    {
      "emoji": null,
      "count": 9460,
      "customEmojiId": "5373223594484587136"
    }
  ],
  "forwardedFrom": null,
  "replyTo": null,
  "author": "Pavel Durov",
  "media": [
    {
      "thumbnailUrl": "https://cdn4.telesco.pe/file/c0ZSQM3RKhQYvFGHRk_MEGdS8C0SZ8Sg5x92lzag1BuxjccjWyYLXjqRMMgGhOy2woVtla0GI-n2BBd75...",
      "duration": null,
      "fileName": null,
      "fileSize": null,
      "type": "video",
      "url": "https://cdn4.telesco.pe/file/f4b10a0c75.mp4?token=fCbX8zIif7OOvVrEXrP2J28QYGwMkltCkPtc4hqOaW9JNyTv0nUoJancuQ3o..."
    }
  ],
  "poll": null,
  "linkPreview": null,
  "links": [],
  "hashtags": [],
  "mentions": [],
  "serviceMessage": false,
  "scrapedAt": "2026-10-11T06:37:27+00:00",
  "input": "durov"
}
```

### What does Telegram Channel Scraper do?

It pages through a channel from the newest message backwards (about 20 messages per request) and returns each message as one row in the Apify dataset, exportable as JSON, CSV or Excel or readable through the API. Typical uses:

- **News and media monitoring.** Track what outlets, governments, brands or crypto projects post, and how many views each post gets.
- **Brand and competitor tracking.** Watch the channels of competitors, with views and reactions as an engagement signal.
- **Crypto and market research.** Collect announcement channels and filter by hashtag, link or mention.
- **OSINT and journalism.** Archive posts with their date, links, forwarded-from source and media URLs before they are edited or deleted.
- **Datasets for AI and RAG.** Get clean message text (and the original HTML) in many languages for analysis, summarising or training pipelines.
- **Recurring feeds.** Use `onlyNewerThan` to pull only the last day or week on a schedule.

### Input

| Field | Type | What it does |
|---|---|---|
| `channels` | list of text | Channel usernames, `@usernames` or t.me links (`https://t.me/durov`, `https://t.me/s/durov`). Prefilled with `durov`, `telegram`. |
| `searchQuery` | text | Optional. Return only messages containing this word or phrase, using Telegram's own in-channel search. Applies to every channel in the list; paging, limits and dates work as usual. |
| `maxMessagesPerChannel` | number | Cap per channel, newest first. Default 100. It is also your message cost cap per channel. Set 0 for profile only: one channel row per channel and no messages. |
| `onlyNewerThan` | text | Optional. Stop paging back when messages are older. A date (`2026-10-01`, `2026-10-01T12:00:00Z`) or relative time (`7 days`, `12 hours`, `2 weeks`). |
| `afterMessageId` | number | Optional. Return only messages with an ID greater than this and stop paging once it is reached. With several channels the same number applies to each. |
| `includeChannelInfo` | true/false | Also return one row per channel. Default true. Message rows carry `channelSubscribers` either way. |
| `requestDelaySeconds` | number | Pause between page requests, default 1.0 s (randomised up to 1.4x). Raise it if Telegram rate-limits you. |
| `proxyConfiguration` | object | Optional Apify Proxy. Default is no proxy. |

```json
{
  "channels": ["durov", "@nexta_live", "https://t.me/s/bbcrussian"],
  "maxMessagesPerChannel": 300,
  "onlyNewerThan": "7 days",
  "includeChannelInfo": true
}
```

Messages are deduplicated per channel and returned newest first. An empty input runs a built-in example: 5 messages from `durov`, no channel row.

#### Recipes

**Profile only (subscriber count, counters, description).** Set `maxMessagesPerChannel` to 0. You get one channel row per channel and no message rows.

```json
{"channels": ["durov", "telegram"], "maxMessagesPerChannel": 0}
```

**Search inside channels.** Set `searchQuery` to a keyword to get only the messages that contain it.

**Scheduled monitoring.** Each run saves a `SUMMARY` record in its key-value store with `newestMessageId` per channel. Pass that number as `afterMessageId` on the next run and you pay only for messages posted since. A channel with no new messages has no `newestMessageId` in that run's `SUMMARY`, so keep the previous number. For several channels, run one input per channel, because the same `afterMessageId` applies to all channels in a run. Alternatively use `onlyNewerThan` (for example `1 day`) on a schedule.

### Output

Two row types share the dataset, told apart by `type`.

**Message row (`type: "message"`)**

| Field | Meaning |
|---|---|
| `messageId`, `channel`, `channelTitle`, `url` | Message number, channel username and title, link `https://t.me/<channel>/<id>`. |
| `date` | Publication time, ISO 8601 UTC. |
| `isEdited` | `true` if Telegram marks the message as edited (the preview does not show when). |
| `text`, `textHtml` | Plain text with line breaks, and the original HTML (bold, links, emoji). `text` strips leading indentation inside code blocks; `textHtml` keeps it. |
| `channelSubscribers` | Subscriber count of the channel at scrape time, repeated on every message row. |
| `views` | Number (Telegram's `1.2K` and `3.4M` are converted). Empty for service messages. |
| `reactions` | List of `{emoji, count}`. Custom emoji carry `customEmojiId`; paid star reactions carry `paid: true`. |
| `forwardedFrom` | `{name, url, author}` when the message is a forward. |
| `replyTo` | `{messageId, url, author, text}` with a snippet of the replied-to message. |
| `author` | Post signature when the channel shows one. |
| `media` | List of `{type, url, thumbnailUrl, duration, fileName, fileSize}` with `type` photo, video, voice, sticker or document. Albums give several entries. |
| `poll` | `{question, type, options: [{text, percent}]}` for polls. |
| `linkPreview` | `{url, siteName, title, description, imageUrl}` for the link card under a message. |
| `links`, `hashtags`, `mentions` | Every link in the text, hashtags (`#Bitcoin`), and `@mentions` without the `@`. |
| `serviceMessage` | `true` for system messages such as "Channel created". |
| `scrapedAt`, `input` | ISO 8601 time of the scrape and the input value the row came from. |

```json
{
  "type": "message",
  "messageId": 124665,
  "channel": "nexta_live",
  "channelTitle": "NEXTA Live",
  "url": "https://t.me/nexta_live/124665",
  "date": "2026-10-09T09:28:30+00:00",
  "isEdited": true,
  "text": "«Рыбарь» ещё вчера наслаждался черногорским дождиком, а спецслужбы уже во всю готовили ему сюрприз ...",
  "textHtml": "<b>«Рыбарь» ещё вчера наслаждался черногорским дождиком ...</b>",
  "channelSubscribers": 772000,
  "views": 24500,
  "reactions": [{"emoji": "👍", "count": 482}, {"emoji": "🔥", "count": 173}],
  "forwardedFrom": null,
  "replyTo": {"messageId": 124662, "url": "https://t.me/nexta_live/124662", "author": "NEXTA Live", "text": "🎣 «Рыбарь» попался ..."},
  "author": null,
  "media": [{"type": "video", "url": null, "thumbnailUrl": "https://cdn4.telesco.pe/file/...", "duration": 37, "fileName": null, "fileSize": null}],
  "poll": null,
  "linkPreview": null,
  "links": [],
  "hashtags": [],
  "mentions": [],
  "serviceMessage": false,
  "scrapedAt": "2026-10-09T09:49:31+00:00",
  "input": "nexta_live"
}
```

**Channel row (`type: "channel"`)**

```json
{
  "type": "channel",
  "url": "https://t.me/nexta_live",
  "title": "NEXTA Live",
  "username": "nexta_live",
  "description": "Медиа №1 из Беларуси ...",
  "subscribers": 772000,
  "photoCount": 69400,
  "videoCount": 43100,
  "fileCount": 3,
  "linkCount": 8570,
  "avatarUrl": "https://cdn4.telesco.pe/file/...",
  "verified": true,
  "scrapedAt": "2026-10-09T09:49:31+00:00",
  "input": "nexta_live"
}
```

Telegram shows large numbers rounded (for example `772K` subscribers), so counts are as precise as Telegram's page. Avatar and media URLs are signed Telegram CDN links that expire, so download them soon. Videos that Telegram's preview does not embed (large files) come back with `url: null` and a thumbnail.

### Pricing

Pay per event, with the prices final:

- **$0.0005 per message** ($0.50 per 1,000 messages), from 21 October 2026
- **$0.001 per channel info row**

You pay only for rows you receive: nothing for channels that are unavailable, failed requests or retries, and the run stops by itself when your **maximum total charge** is reached. Platform usage is included.

Example: 10 channels with 500 messages each and channel info on is 5,000 messages and 10 channel rows, so 5,000 x $0.0005 + 10 x $0.001 = **$2.51**. A daily run of 1 channel with `onlyNewerThan: "1 day"` and about 30 posts costs about $0.015 per day.

### Use it from the API

```bash
curl -X POST "https://api.apify.com/v2/acts/datagleaner~telegram-channel-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"channels": ["durov"], "maxMessagesPerChannel": 50}'
```

It also works from n8n, Make or Zapier through the Apify integrations.

### Use with Python

Install the client with `pip install apify-client` and set `APIFY_TOKEN`. This run returns at most 10 messages and 1 channel row, so it costs at most $0.006.

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("datagleaner/telegram-channel-scraper").call(run_input={
    "channels": ["durov"],
    "maxMessagesPerChannel": 10,
})
for item in client.dataset(run.default_dataset_id).iterate_items():
    if item["type"] == "message":
        print(item["date"], item["views"], (item["text"] or "")[:80])
```

### Use with JavaScript / Node.js

Install the client with `npm install apify-client` and set `APIFY_TOKEN`. Save as `run.mjs` and run `node run.mjs`.

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('datagleaner/telegram-channel-scraper').call({
    channels: ['durov'],
    maxMessagesPerChannel: 10,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) {
    if (item.type === 'message') console.log(item.date, item.views, (item.text || '').slice(0, 80));
}
```

### Use it from n8n, Make, Zapier or an AI agent

Actor ID: `datagleaner/telegram-channel-scraper`

Minimal input:

```
{"channels": ["durov"], "maxMessagesPerChannel": 10}
```

Each tool below runs this Actor with your own Apify API token.

- **n8n:** add the **Apify** node (`@apify/n8n-nodes-apify`). On n8n Cloud you install it from the community node registry. Choose **Run an Actor and get dataset**, set Actor to `datagleaner/telegram-channel-scraper` and paste the input above.

- **Make:** use the Apify app's **Run an Actor** module, then **Get Dataset Items** to read the results. **Watch Actor Runs** can trigger a scenario when a run finishes.

- **Zapier:** use the Apify action **Run Actor**, then the search **Fetch dataset items**. The trigger **Finished Actor run** starts a Zap when a run ends.

- **AI agents (MCP):** connect to `https://mcp.apify.com?tools=datagleaner/telegram-channel-scraper`. In Claude Code:

  ```
  claude mcp add --transport http apify "https://mcp.apify.com?tools=datagleaner/telegram-channel-scraper"
  ```

  Then run `/mcp` to sign in to Apify in your browser. To use a token instead, add `--header "Authorization: Bearer YOUR_APIFY_TOKEN"`. Then ask in plain words, for example:

  > Use datagleaner/telegram-channel-scraper to get the last 7 days of posts from the Telegram channels durov and telegram, then list the 5 posts with the most views and summarise each in one line.

- **LangChain (Python):**

```python
## pip install langchain-apify, then set APIFY_TOKEN in your environment
import json
from langchain_apify import ApifyActorsTool
tool = ApifyActorsTool("datagleaner/telegram-channel-scraper")
result = tool.invoke({"run_input": json.loads('{"channels": ["durov"], "maxMessagesPerChannel": 10}')})
```

### Limits

- **Public channels with the web preview enabled only.** If `t.me/s/<channel>` redirects to the plain `t.me/<channel>` page, the channel has no preview (or does not exist) and is skipped with a warning. Private channels, groups, invite links (`t.me/+...`, `joinchat`) and bots are not supported.
- **No member lists, no comments, no private data.** The Actor returns what any visitor of the preview sees: posts, counters and media links. It cannot list subscribers or read replies posted in a linked discussion group.
- **Documents and some media are not shown by Telegram's preview.** Messages carrying only such media come back with their text, date and views but may have an empty `media` list; the unsupported media notice is not exposed as data.
- **Counts are rounded** by Telegram (`1.2K`), and channels can hide view counts.
- **History depth** is whatever Telegram serves by paging back; very long histories take many requests (about 20 messages per request at 1 second each).
- Telegram rate limits are handled with backoff. If a channel keeps answering 429, raise `requestDelaySeconds` or enable `proxyConfiguration`.

### FAQ

**Do I need a Telegram account, API key or phone number?** No. The Actor reads the public web preview page and needs no login of any kind.

**Can it scrape Telegram channel members or subscribers?** No. Telegram's public preview does not list members. The channel row gives the subscriber count, rounded as Telegram shows it.

**Can it scrape private channels or groups?** No. Only public channels whose web preview is on. A channel that does not exist, is private or has its preview off is skipped, and the log and the `SUMMARY` record name it with the reason.

**Can I scrape only new posts on a schedule?** Yes: set `onlyNewerThan` (for example `1 day`), or pass the `newestMessageId` from the last run's `SUMMARY` as `afterMessageId`, and schedule the Actor.

**Can I download the photos and videos, or export to CSV?** The rows hold Telegram CDN URLs (`media[].url`, `thumbnailUrl`) that you download with your own tooling; the links are signed and expire. The dataset exports as JSON, CSV or Excel from the run page or the API.

### Responsible use

This Actor reads only publicly visible channel previews and paces its requests politely. Messages can contain personal data (names, usernames, phone numbers in text): **you** are responsible for a lawful basis, for respecting Telegram's terms and channel owners' rights, and for your obligations under GDPR and any other law that applies. Do not use the output to harass, profile private individuals or evade platform rules. This is not legal advice.

# Actor input Schema

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

Channel usernames, @usernames, or t.me links (https://t.me/durov, https://t.me/s/durov). Only public channels whose web preview is enabled work; private channels, groups and invite links do not.

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

Optional. Return only messages that contain this word or phrase, using Telegram's own in-channel search (t.me/s/<channel>?q=...). Works with every channel in the list, with paging, limits and dates as usual. Leave empty for all messages.

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

Upper bound on messages returned for each channel, newest first. Each returned message is billed, so this is also your cost cap per channel. Set 0 for profile only: one channel row per channel (subscribers, counters, description, avatar, verified) and no messages.

## `onlyNewerThan` (type: `string`):

Optional. Stop paging back once messages are older than this. A date or datetime such as 2026-10-01 or 2026-10-01T12:00:00Z, or a relative time such as 7 days, 12 hours, 2 weeks.

## `afterMessageId` (type: `integer`):

Optional. Return only messages whose ID is greater than this number and stop paging once it is reached. For scheduled monitoring of one channel: put the newest messageId from your last run here (each run's SUMMARY record lists newestMessageId per channel). With several channels the same number applies to each.

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

Also return one item per channel with title, username, description, subscribers, photoCount/videoCount/fileCount/linkCount, avatar and verified flag. Billed as one channel event per channel. Every message row also carries channelSubscribers either way.

## `requestDelaySeconds` (type: `number`):

Base pause between page requests to Telegram; the actual pause is randomised up to 1.4x. Raise it if Telegram answers with rate limits (HTTP 429).

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

Optional. The default is no proxy, which works from Apify's servers. Enable Apify Proxy only if Telegram rate-limits you.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "telegram"
  ],
  "maxMessagesPerChannel": 100,
  "includeChannelInfo": true,
  "requestDelaySeconds": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `messages` (type: `string`):

One row per message, newest first

## `channels` (type: `string`):

One row per channel with subscribers and counters

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

Every row with every field

# 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"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("datagleaner/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 = {
    "channels": [
        "durov",
        "telegram",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

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

```

## MCP server setup

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