# Telegram Channel Scraper: Posts & Views · $1/1k (`impressionable_lupine/telegram-channel-scraper`) Actor

Telegram channel scraper for public channels: posts, views, dates, media, links, forwards and channel info (title, subscribers) from t.me/s. No login or API keys. Since-date, keyword and only-new filters. $0.001 per post.

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

## Pricing

from $1.00 / 1,000 post delivereds

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 and channel info from public channels

**Telegram Channel Scraper** pulls posts from any **public Telegram channel** through Telegram's own public web preview (`https://t.me/s/<channel>`). For each post you get the text, date, view count, media, links, forward and reply info. For each channel you get the title, description, subscriber count and photo. Results come back as clean JSON, CSV or Excel.

- **No login, no phone number, no API keys.** The Actor reads the same public page anyone can open in a browser. It doesn't use MTProto, user sessions or any Telegram account.
- **Views as numbers.** "5.83M" becomes `5830000`, so you can sort and chart engagement straight away.
- **History, not just the latest 20.** The Actor pages backwards with `?before=<postId>` until it reaches your `maxPostsPerChannel`, your `since` date or the first post of the channel.
- **Monitoring mode.** Turn on `onlyNewSinceLastRun` and schedule the Actor: each run returns (and charges for) only posts you haven't seen yet.
- **$0.001 per post ($1 per 1,000).** Private, missing or unreachable channels come back as free `error` rows.

### Why use this Telegram channel scraper?

| | Telegram Channel Scraper | Telegram API (MTProto) tools | Copy-paste by hand |
|---|---|---|---|
| Account needed | None | A phone number and API ID/hash; sessions can get banned | None |
| Setup | Paste channel names, click Start | Login flow, 2FA, session files | — |
| Views | Numeric `views` on every post | Yes | Visible, but "1.2K" text |
| History | Pages back as far as the channel's public preview goes | Yes | Slow scrolling |
| Personal data | None: channel-authored content only | Can expose member and commenter data | — |
| Price | **$0.001 per post** | Your servers plus account risk | Your time |

#### Who uses it

- **Media monitoring and PR teams:** track what news, crypto or brand channels publish, and how many views each post gets.
- **Researchers and analysts:** build datasets of public channel posts by date range or keyword.
- **Marketers:** compare channels by subscribers and per-post views before you buy an ad placement.
- **AI agents and RAG pipelines:** feed fresh channel posts to an LLM with one tool call.

### How to use it

1. Click **Try for free**. The form is prefilled with `telegram` and `durov` and 20 posts per channel.
2. Add the channels you want: `bloomberg`, `@durov` or `https://t.me/s/telegram` all work.
3. Optionally set **Only posts since**, **Keywords** or **Only new posts since the last run**.
4. Click **Start**. The prefilled run finishes in about 5 seconds. Open the **Posts** or **Channels** view, or export as JSON, CSV or Excel.
5. For monitoring, turn on **Only new posts since the last run** and add a **Schedule** (for example, hourly).

### Input

Only `channels` is required.

| Field | Type | What it does |
|---|---|---|
| `channels` | string\[] | Public channel usernames, `@names` or `t.me` links. The same channel typed twice is scraped once. Invite links (`t.me/+...`), `t.me/c/...` links, users, bots and groups become free `error` rows. |
| `maxPostsPerChannel` | integer | Maximum posts per channel, newest first. Default `50`. |
| `since` | string | Stop at posts older than this date. Accepts `2026-10-01`, an ISO timestamp, or a relative period such as `7 days` or `12 hours`. |
| `keywords` | string\[] | Keep only posts whose text, link-preview title or description, or document name contains these words. Case-insensitive. |
| `keywordMode` | `any` | `all` | `any` (default): at least one keyword must match. `all`: every keyword must match. |
| `includeChannelInfo` | boolean | Add one `channel` record per channel (title, description, subscribers, photo). Default `true`. Charged as `channel-resolved`. With `onlyNewSinceLastRun`, the record is added only when the channel has new posts. |
| `onlyNewSinceLastRun` | boolean | Skip posts this saved search returned before. History lives in a named key-value store. |
| `stateKey` | string | Optional monitor name. By default each keyword filter gets its own history. |
| `maxPagesPerChannel` | integer | Safety cap on preview pages (about 20 posts each) per channel. Default `50`. |
| `requestDelayMs` | integer | Pause between page requests. Default `1000`, minimum `500`. |
| `maxConcurrency` | integer | Channels scraped in parallel. Default `2`, maximum `5`. |

#### Input example: hourly monitor of three channels

```json
{
  "channels": ["telegram", "durov", "https://t.me/s/bloomberg"],
  "maxPostsPerChannel": 100,
  "since": "7 days",
  "keywords": ["ai", "bitcoin", "update"],
  "onlyNewSinceLastRun": true,
  "stateKey": "tech-news-monitor"
}
```

### Output

Every dataset item has a `type`: `post`, `channel` or `error`. Here is a real `post` from a local test run on 2026-10-22 (CDN URL and links shortened):

```json
{
  "type": "post",
  "channel": "telegram",
  "channelTitle": "Telegram News",
  "postId": 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": 2020000,
  "edited": false,
  "hasMedia": false,
  "mediaTypes": [],
  "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, …",
    "imageUrl": "https://cdn4.telesco.pe/file/R_08vaIZo8g…"
  },
  "links": ["https://telegram.org/blog/welcome-messages-buttons-TG-13", "https://t.me/telegram/455", "…"],
  "isForwarded": false,
  "forwardedFrom": null,
  "forwardedFromUrl": null,
  "replyTo": null,
  "matchedKeywords": [],
  "isNew": null,
  "scrapedAt": "2026-10-22T05:09:58.580Z"
}
```

A `channel` record:

```json
{
  "type": "channel",
  "channel": "durov",
  "url": "https://t.me/durov",
  "title": "Pavel Durov",
  "description": "Founder of Telegram.",
  "subscribers": 10300000,
  "photoUrl": "https://cdn4.telesco.pe/file/…jpg",
  "verified": true,
  "counters": { "photos": 102, "videos": 47, "files": null, "links": 200 },
  "scrapedAt": "2026-10-22T05:09:58.580Z"
}
```

An `error` row (free):

```json
{ "type": "error", "channel": "no_such_channel_here", "input": "no_such_channel_here", "url": "https://t.me/no_such_channel_here", "errorCode": "CHANNEL_NOT_FOUND_OR_PRIVATE", "errorMessage": "…Nothing was charged." }
```

- **Media.** `media[]` lists each item with its `type` (`photo`, `video`, `roundVideo`, `document`, `audio`, `voice`, `sticker`, `poll`, `location`, `unsupported`), the image or video `url` when the preview exposes it, a video `thumbnailUrl`, and `title`/`size` for documents. Albums list every photo.
- **Forwards.** `isForwarded` is always set. `forwardedFrom` (a channel title) and `forwardedFromUrl` are filled only when the forward links to a public channel post. Names of individual users are never collected.
- **Replies.** `replyTo` holds the `channel`, `postId` and `url` of the post being answered, with no author names.
- **Run summary.** The run's `OUTPUT` record (key-value store) holds the status, each channel's health and stop reason, counts, billing and the `onlyNew` state.

The dataset has four ready-made views: **Posts**, **Media & links**, **Channels** and **Skipped channels**.

### Pricing

Pay per event. Platform usage is included.

| Event | Price | When it is charged |
|---|---|---|
| `message-scraped` | **$0.001** per post | One post written to the dataset, after your `since`, keyword and `onlyNew` filters |
| `channel-resolved` | **$0.003** per channel | One channel-info record written to the dataset (only when `includeChannelInfo` is on; in `onlyNewSinceLastRun` mode only when the channel has new posts) |

| Run | Records | Cost |
|---|---|---|
| Prefilled example (2 channels × 20 posts) | 2 channels + 40 posts | $0.046 |
| 10 channels × 100 posts | 10 channels + 1,000 posts | $1.03 |
| Hourly monitor with `onlyNewSinceLastRun`, 5 channels | 0–20 new posts (+ a channel record per channel with news) | $0 when nothing is new; ~$0.003–$0.04 otherwise |

You are never charged for:

- private, missing, invalid or unreachable channels (`error` rows);
- posts removed by your `since`, keyword or `onlyNew` filters;
- service messages ("channel created", "photo updated").

Set **Max cost per run** (`maxTotalChargeUsd`) and the Actor stops cleanly at that budget: it pushes only records it can charge for and marks the run `budget_limit_reached`. If the budget can't cover even the first record ($0.003 for a channel record, or $0.001 for a post when `includeChannelInfo` is off), the run ends at once with `BUDGET_TOO_LOW` and fetches nothing.

### API and integrations

Copy-paste ready: set `APIFY_TOKEN` to your own token from Apify Console → Settings → API & Integrations.

**HTTP (one call, returns the records):**

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

**JavaScript:**

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('impressionable_lupine/telegram-channel-scraper').call({ channels: ['bloomberg'], since: '3 days', includeChannelInfo: false });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python:**

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("impressionable_lupine/telegram-channel-scraper").call(run_input={"channels": ["durov"], "keywords": ["ton"], "maxPostsPerChannel": 200})
posts = [i for i in client.dataset(run["defaultDatasetId"]).list_items().items if i["type"] == "post"]
```

Results also flow to Zapier, Make, n8n, Google Sheets, Slack and webhooks through Apify integrations. A common setup is an hourly schedule with `onlyNewSinceLastRun`, plus a webhook that posts the new items to Slack.

### Use with AI agents (MCP)

The Actor works as a tool through the [Apify MCP server](https://mcp.apify.com) in Claude, Cursor, VS Code and any MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?tools=impressionable_lupine/telegram-channel-scraper",
      "headers": { "Authorization": "Bearer <APIFY_TOKEN>" }
    }
  }
}
```

Example prompt: *"Use telegram-channel-scraper to get the last 30 posts from @durov and @telegram, then list the five most-viewed posts with their date, views and link."*

Agents get a flat, predictable schema with numeric `views`, ISO dates and canonical `url`s, at $0.001 per post.

### Limitations

- **Public channels only.** The Actor reads `t.me/s/<channel>`, which exists only for public channels whose owners haven't turned off the web preview. Private channels, invite links, groups, users and bots are skipped as free `error` rows.
- **What the preview shows.** View counts are rounded as Telegram displays them ("1.2K" → `1200`). Reactions, comments and comment counts aren't collected. Some media (rich posts, some animations) shows as `unsupported` with a link to the post.
- **Media URLs expire.** Photo and video URLs point to Telegram's CDN and are signed. Download what you need soon after the run.
- **History depth.** Paging goes back as far as the public preview allows. Very large backfills take time, because the Actor waits `requestDelayMs` between pages.
- **Forward sources.** The preview often hides the source of a forward. `forwardedFrom` is filled only when it links to a public channel.

### FAQ

**Is it legal to scrape Telegram channels?**
This Actor reads only public channel pages that Telegram serves to anyone without login, and t.me publishes no robots.txt restrictions (checked 2026-10-22). It collects content the channel itself published, not personal data about subscribers or commenters. You're still responsible for how you use the data: respect copyright, Telegram's Terms of Service and privacy laws such as GDPR, and don't use the output to profile individuals. This isn't legal advice; if in doubt, ask a lawyer.

**Can it scrape private channels or groups?**
No. It has no login and doesn't join chats. Private channels, invite links (`t.me/+...`), `t.me/c/...` links and groups are reported as `error` rows and never charged.

**Does it collect user data or phone numbers?**
No. There are no member lists, commenter profiles or phone numbers. Post signatures (admin names), the names of users that posts were forwarded from, and reply author names are deliberately left out.

**Why is a channel showing CHANNEL_NOT_FOUND_OR_PRIVATE when it opens in my app?**
The channel may be private, restricted in your region, have the web preview turned off, or the username may belong to a user, bot or group. Check that `https://t.me/s/<name>` opens in a browser.

**How does "only new posts" work?**
For each channel, the Actor stores the IDs of the posts it returned (and the highest ID) in a named key-value store (`telegram-channel-scraper-state`), under a key derived from your keyword filter or your `stateKey`. The next run stops paging when it reaches posts it has already seen, so you pay only for new ones.

**How fast is it?**
About 1 second per page of 20 posts per channel, with 2 channels in parallel by default. 1,000 posts from one channel take about a minute.

**Do I need proxies?**
No. The Actor makes a few polite requests per channel and uses no proxies.

# Actor input Schema

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

Public channel usernames, @names or t.me links, e.g. "telegram", "@durov", "https://t.me/s/bloomberg". Private channels, invite links (t.me/+...), groups, users and bots are skipped and reported as free "error" rows.

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

Maximum posts returned (and charged) per channel, newest first. The scraper pages backwards 20 posts at a time.

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

Stop at posts older than this date. Absolute (YYYY-MM-DD or ISO timestamp) or relative ("7 days", "12 hours", "2 weeks"). Leave empty for no date limit.

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

Keep only posts whose text (or link-preview title/description, or document name) contains at least one of these words or phrases. Case-insensitive. Leave empty for all posts.

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

"any" = at least one keyword must match; "all" = every keyword must match.

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

Add one "channel" record per public channel: title, description, subscriber count, photo URL, verified flag and media counters. Charged as channel-resolved ($0.003). Turn off to get posts only.

## `onlyNewSinceLastRun` (type: `boolean`):

Remember returned post IDs per channel in a named key-value store and skip them next time. Ideal for scheduled monitoring: you only pay for posts you have not seen.

## `stateKey` (type: `string`):

History key for "Only new posts". Default: derived from your keyword filter, so each saved search has its own history. Set the same name on several tasks to share one history.

## `stateStoreName` (type: `string`):

Named key-value store that keeps the "Only new posts" history (letters, digits, hyphens).

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

Safety cap on preview pages (about 20 posts each) fetched per channel, useful with strict keyword filters.

## `requestDelayMs` (type: `integer`):

Pause between page requests of a worker. Minimum 500 ms to stay polite to t.me.

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

How many channels are scraped at the same time (each channel pages sequentially).

## Actor input object example

```json
{
  "channels": [
    "telegram",
    "durov"
  ],
  "maxPostsPerChannel": 20,
  "keywordMode": "any",
  "includeChannelInfo": true,
  "onlyNewSinceLastRun": false,
  "stateStoreName": "telegram-channel-scraper-state",
  "maxPagesPerChannel": 50,
  "requestDelayMs": 1000,
  "maxConcurrency": 2
}
```

# Actor output Schema

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

Channel posts, newest first per channel, with views, media and links.

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

One record per public channel: title, description, subscribers, photo.

## `allJson` (type: `string`):

No description

## `summary` (type: `string`):

OUTPUT record: status, per-channel health, counts, billing and onlyNew state.

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

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

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

```

## MCP server setup

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