# Telegram Channel Scraper: posts, views, media, no API key (`thatmike1/telegram-channel-scraper`) Actor

Scrape public Telegram channels without a login or API key: post text, views, reactions, media links, polls, replies, forward sources and channel subscriber counts.

- **URL**: https://apify.com/thatmike1/telegram-channel-scraper.md
- **Developed by:** [Michal Pšenčík](https://apify.com/thatmike1) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.005 / actor start

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Telegram Channel Scraper (posts, views, media, no API key)

### What does Telegram Channel Scraper do?

This Actor scrapes **public Telegram channels** through Telegram's own web preview at
`t.me/s/<channel>`. For every post it returns the text, the exact posting time in UTC, the view
count, reactions, links to any photos, videos and voice notes, the poll results, the post it replies
to, the channel it was forwarded from, and a link to the post itself. Each channel also produces one
record with its title, description and subscriber count.

**No Telegram account, no phone number, no API key, no `api_id`.** The Actor reads the same public
page a browser would, so there is nothing to authorise and nothing to keep logged in.

Give it a list of channel usernames, optionally a keyword filter and a start date, and run it. Because
it runs on the Apify platform you get a REST API for the results, scheduled runs, proxy rotation, run
monitoring, and integrations with Google Sheets, Zapier, Make, Slack and the rest.

Try it from the **Input** tab: leave everything at its default and hit Start to get the latest posts
from `@telegram` and `@durov`.

### Why use Telegram Channel Scraper?

- **Monitoring and alerting.** Run it on a schedule with a keyword filter to watch a set of channels
  for a brand, a ticker, a place name or a competitor, and push the hits into Slack or a webhook.
- **OSINT and news desks.** Views, reactions and forward provenance are the reach signals on Telegram,
  and all three come back with every post.
- **Research datasets.** Page backwards through a channel's history with `since` and build a corpus
  you can rerun over time. `messageId` is stable, so reruns upsert cleanly.
- **Market and crypto signals.** Channel calls, their timing and their view counts, straight into a
  spreadsheet or a database.
- **No MTProto client to maintain.** Telegram's own API needs an `api_id`, a session file and a phone
  number, and rate-limits per account. This Actor needs none of that.

### How to use Telegram Channel Scraper

1. Click **Try for free** (or open the Actor in Apify Console).
2. On the **Input** tab, put the channels you want in **Channels**. A username, an `@username` or a
   `t.me` link all work.
3. Set **Maximum messages per channel**. The default is 40, which is two pages of the preview.
4. Optionally add a **Keyword filter**, or a date under **Only messages posted since**, to narrow the run.
5. Click **Start** and watch the log. Forty messages from two channels takes about two seconds.
6. Open the **Output** tab (or **Storage → Dataset**) and export as JSON, CSV, Excel or HTML, or pull it
   through the [Apify API](https://docs.apify.com/api/v2).

To keep the data fresh, use **Schedules** in Apify Console to run the Actor every few minutes for a
monitoring feed, or once a morning for a daily digest.

### Input

All fields are optional; the defaults return the latest 40 posts from `@telegram` and `@durov`.

| Field                | Type    | Default                 | Description                                                                                                               |
| -------------------- | ------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `channels`           | array   | `["telegram", "durov"]` | Channels to scrape. A username, an `@username` or any `t.me` link.                                                        |
| `maxItems`           | integer | `40`                    | Stop after pushing this many messages **per channel**. `0` means no limit.                                                |
| `keywords`           | array   | `[]`                    | Only return messages whose text contains one of these words or phrases, matched case-insensitively.                       |
| `since`              | string  | empty                   | Skip messages older than this date and stop paging back at it. `YYYY-MM-DD` or a full ISO 8601 timestamp.                 |
| `maxScannedMessages` | integer | `500`                   | How far back to read per channel before giving up on finding more keyword matches. `0` means no limit.                    |
| `proxyConfiguration` | object  | Apify proxy disabled    | Optional. The preview serves these pages without an anti-bot challenge, so datacenter proxies are plenty if you want one. |

Example input:

```json
{
    "channels": ["telegram", "durov", "https://t.me/nexta_live"],
    "keywords": ["gift", "sticker"],
    "since": "2026-06-01",
    "maxItems": 100
}
```

### Output

Each post is one dataset item with `"type": "message"`. Each channel adds one item with
`"type": "channel"` before its messages. Download the dataset as JSON, HTML, CSV or Excel.

The channel record:

```json
{
    "type": "channel",
    "channel": "durov",
    "title": "Pavel Durov",
    "description": "Founder of Telegram.",
    "url": "https://t.me/durov",
    "imageUrl": "https://cdn4.telesco.pe/file/HYnMQg4YKLOXAKQEfmxf0NzfSxGdMvCScrzLMtLj6Mx…",
    "subscribers": 10900000,
    "subscribersText": "10.9M",
    "photosCount": 102,
    "videosCount": 46,
    "filesCount": null,
    "linksCount": 198,
    "scrapedAt": "2026-09-03T17:02:12.041Z"
}
```

A post with a video and a link preview:

```json
{
    "type": "message",
    "channel": "telegram",
    "messageId": 460,
    "postId": "telegram/460",
    "url": "https://t.me/telegram/460",
    "date": "2026-08-26T19:12:34.000Z",
    "text": "For all the info from today's update and a look back at 13 years of Telegram features — check out our blog:\nhttps://telegram.org/blog/welcome-messages-buttons-TG-13\n\nAugust Features\n1 • 2 • 3 • 4 • 5 • More",
    "views": 843000,
    "viewsText": "843K",
    "forwardedFrom": null,
    "replyTo": null,
    "media": [],
    "linkPreview": {
        "url": "https://telegram.org/blog/welcome-messages-buttons-TG-13",
        "siteName": "Telegram",
        "title": "Welcome Messages, Buttons in Messages and Signed Gifts",
        "description": "Introducing Welcome Messages to greet new members of groups or channels, Buttons in Messages to make any text interactive, Inline Documents in Messages and much more.",
        "imageUrl": "https://cdn4.telesco.pe/file/dhX3eCd_S2LrqYN1CQrGUhmYQarf3dPyUfR0Z_uOtu6…"
    },
    "outlinks": ["https://telegram.org/blog/welcome-messages-buttons-TG-13", "https://t.me/telegram/455"],
    "reactions": [],
    "poll": null,
    "authorSignature": null,
    "edited": false,
    "scrapedAt": "2026-09-03T17:02:11.938Z"
}
```

A poll comes back with its options and the share of the vote each one holds:

```json
{
    "type": "message",
    "channel": "polls",
    "messageId": 122,
    "url": "https://t.me/polls/122",
    "date": "2026-09-03T08:00:15.000Z",
    "text": "",
    "views": 8,
    "poll": {
        "question": "What's the most realistic path for China-West relations over the next decade?",
        "type": "Anonymous Poll",
        "voters": 1,
        "options": [
            { "text": "Managed rivalry with guardrails", "percent": 0 },
            { "text": "Full economic decoupling", "percent": 0 },
            { "text": "Gradual cooperation on shared crises", "percent": 100 },
            { "text": "Escalating conflict with no resolution", "percent": 0 }
        ]
    }
}
```

A reply carries the post it answers, a forward carries its source, and files come back as a name and a
size:

```json
{
    "replyTo": {
        "messageId": 123066,
        "url": "https://t.me/nexta_live/123066",
        "author": "NEXTA Live",
        "text": "Россияне атаковали университет в центре Киева — в это время у студентов шли занятия…"
    },
    "forwardedFrom": { "name": "LUXTA", "channel": "luxta_tv", "url": "https://t.me/luxta_tv/8235" },
    "media": [{ "type": "document", "url": null, "fileName": "Конкретная математика.zip", "fileSize": "15.4 MB" }],
    "reactions": [
        { "emoji": "🤬", "emojiId": null, "count": 1280 },
        { "emoji": "😢", "emojiId": null, "count": 173 }
    ]
}
```

CDN links in the examples above are shortened with `…` for readability; the real values are full URLs.

#### Data fields

| Field             | Type           | Description                                                                                       |
| ----------------- | -------------- | ------------------------------------------------------------------------------------------------- |
| `type`            | string         | `message` or `channel`. Filter on this if you only want one of them.                              |
| `channel`         | string         | Channel username, without the `@`.                                                                |
| `messageId`       | number         | Telegram's post number inside the channel. Stable, so use `channel` + `messageId` as your key.    |
| `postId`          | string         | `<channel>/<messageId>`, the single-field version of the same key.                                |
| `url`             | string         | Link to the post on t.me.                                                                         |
| `date`            | string | null | When it was posted, ISO 8601 in UTC.                                                              |
| `text`            | string         | Post text with line breaks preserved and emoji intact. Empty for media-only posts.                |
| `views`           | number | null | View count, expanded from the preview's `843K` shorthand.                                         |
| `viewsText`       | string | null | The shorthand as the preview renders it.                                                          |
| `forwardedFrom`   | object | null | `{ name, channel, url }` for a forwarded post. `channel` and `url` are null for a private source. |
| `replyTo`         | object | null | `{ messageId, url, author, text }` of the post being replied to.                                  |
| `media`           | array          | One entry per attachment: `{ type, url, thumbnailUrl, duration, fileName, fileSize }`.            |
| `linkPreview`     | object | null | The preview card: `{ url, siteName, title, description, imageUrl }`.                              |
| `outlinks`        | array          | Every http(s) link inside the post text, deduplicated.                                            |
| `reactions`       | array          | `{ emoji, emojiId, count }` per reaction. Custom emoji have an id and no character.               |
| `poll`            | object | null | `{ question, type, voters, options[] }`, each option with its text and percent.                   |
| `authorSignature` | string | null | The signature on the post, when the channel signs posts.                                          |
| `edited`          | boolean        | Whether the post carries Telegram's "edited" mark.                                                |
| `scrapedAt`       | string         | When this run read the page, ISO 8601 in UTC.                                                     |

Media `type` is one of `photo`, `video`, `roundvideo`, `voice`, `document` or `sticker`. Photos, videos
and voice notes come back with a direct CDN URL. Documents do not: Telegram's preview publishes the file
name and size but no download link, so `url` is null for them.

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

This Actor uses **pay per event** pricing: **$0.002 per result** plus **$0.005 per run start**. Platform
usage is included, so those two numbers are the whole bill. A result is one message, or the one channel
record each channel produces.

The default input returns 82 results, which costs about **$0.17**. A hundred messages from a single
channel costs **$0.21**. A keyword-filtered monitoring run is much cheaper, because you are only charged
for the messages that match: scanning 500 messages for two keywords and finding four of them costs
**$0.02**. The Apify free plan comes with $5 of monthly platform credit, which is enough to try the Actor
and run it daily for a while.

To cap spending on a single run, set a maximum cost in the run options, or use `maxItems`.

### Tips and advanced options

- **Cut the bill on monitoring runs.** Set `keywords` and a small `maxItems`. You are only charged for
  messages that are pushed, so a quiet interval costs one actor start.
- **`maxItems` is per channel, not per run.** Ten channels at `maxItems: 50` is up to 500 messages. This
  is deliberate: a shared budget would let the first channel eat all of it.
- **Deduplicate across runs.** `postId` is stable. A post scraped twice will have the same id and a higher
  view count the second time, which is exactly what you want for tracking reach over time.
- **Reading a long history.** `since` is the cheap way to bound it. Without `since` or `maxItems`, the
  Actor pages back through the whole channel, twenty posts at a time.
- **Keyword runs need a scan budget.** A keyword that never appears would otherwise walk the entire
  history, so `maxScannedMessages` (500 by default) stops the search. Raise it to dig deeper.
- **Proxies are optional.** Telegram does not challenge these pages. Turn on Apify Proxy datacenter groups
  only if you are running very frequently from one address.

### FAQ, disclaimers and support

**Does this Actor need a Telegram account or API key?**
No. It reads the public web preview at `t.me/s/<channel>`, which needs no login, no phone number and no
`api_id`.

**Which channels can it read?**
Any public channel that has a web preview. Private and invite-only channels, groups, user accounts and
bots have no preview: t.me redirects those to a plain landing page, and the Actor logs a warning and
skips them rather than failing the run.

**Does it return forward counts?**
No, and no scraper of this page can. Telegram's public preview publishes views and reactions but not the
number of times a post was forwarded. What the Actor does return is `forwardedFrom`, the channel a post
was forwarded **from**.

**Does it return comments or discussion-group replies?**
No. `replyTo` is the in-channel reply chain. Comments live in the linked discussion group, which the
channel preview does not render.

**Why is `text` sometimes empty?**
A post can be media only, a poll only, or a forward with no caption. The text field is empty rather than
null in those cases, and the content is in `media` or `poll`.

**Why did I get fewer messages than I asked for?**
Check `maxItems`, `keywords` and `since`, and remember the channel may simply be shorter than your limit.
The Actor also stops at the first post of a channel.

**Is scraping Telegram legal?**
This Actor reads only publicly available pages, the same ones anyone can open without an account. Scraping
publicly accessible information is generally legal in the EU and the US, but you are responsible for how
you use the output, including Telegram's Terms of Service, the privacy of the people posting, and any
rights that apply where you operate. Talk to a lawyer if you plan to redistribute the data commercially.

**Found a bug, or need something else?**
Open an issue on the **Issues** tab of this Actor and it will be picked up. If you need a custom version,
Telegram-wide search, or an extraction that is not covered here, mention it in the issue.

# Actor input Schema

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

Public Telegram channels to scrape. Accepts a username, an @username or a t.me link. Only channels with a public web preview at t.me/s/<channel> can be read; private channels, groups and bots are skipped with a warning.

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

Stop after pushing this many messages for each channel. Set to 0 for no limit. The channel record itself is not counted.

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

Only return messages whose text contains one of these words or phrases, matched case-insensitively. Leave empty to return every message.

## `since` (type: `string`):

Skip messages older than this date and stop paging back once the Actor reaches it. Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Leave empty for no date limit.

## `maxScannedMessages` (type: `integer`):

How far back to read before giving up on finding more matches. Only relevant with a keyword filter, which can otherwise walk a whole channel history. Set to 0 for no limit.

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

The Telegram web preview serves these pages without an anti-bot challenge, so a proxy is optional. Datacenter proxies are enough if you want one.

## Actor input object example

```json
{
  "channels": [
    "telegram",
    "durov"
  ],
  "maxItems": 40,
  "keywords": [],
  "since": "",
  "maxScannedMessages": 500,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `messages` (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 = {
    "channels": [
        "telegram",
        "durov"
    ],
    "maxItems": 40
};

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

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

```

## MCP server setup

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