# Telegram Channel Scraper: Messages, Views & Channel Info (`changefeeds/telegram-channel-scraper`) Actor

Export public Telegram channel messages — text, view counts, media type, links, hashtags, reply and edit info — plus channel title, description and subscriber count. No Telegram login, no phone number, no API ID: reads the public t.me/s preview.

- **URL**: https://apify.com/changefeeds/telegram-channel-scraper.md
- **Developed by:** [Changefeeds Tools](https://apify.com/changefeeds) (community)
- **Categories:** Social media, Lead generation
- **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 & Channel Info

Give it a list of public Telegram channels and it exports their messages —
text, views, hashtags, links, media type, reply and edit info — plus one row
per channel with title, description, subscriber count and photo. Built for
"telegram scraper" / "telegram channel messages export" jobs: research
digests, competitor monitoring, dataset building, archiving a public channel's
posts. No Telegram login, no API ID, no phone number — it reads the same
public `t.me/s/<channel>` preview a logged-out browser sees.

This is a plain, one-shot export. If you need a run-over-run *change feed*
(only what's new, edited, deleted or view-changed since the last check), see
our companion actor **Telegram Channel Monitor – Change Feed**.

### Input

| Field | Default | Notes |
|---|---|---|
| `channels` | required | Channel usernames: `durov`, `@durov`, `t.me/durov`, `https://t.me/s/durov` all work. Up to 1,000 per run. Invalid entries become an error row; the run continues. |
| `maxMessagesPerChannel` | 200 | 1 to 10,000. Pages backwards through the preview using its `?before=<id>` cursor. |
| `since` | none | ISO date/time. Only messages on or after it are exported; pagination stops once it reaches older ones. |
| `until` | none | ISO date/time. Only messages on or before it are exported. |
| `keywords` | none | Keep only messages whose text contains at least one keyword (case-insensitive). Leave empty to keep everything. |
| `includeChannelInfo` | true | Also emit one row per channel with title, description, subscribers, photo, verified badge. |

```json
{
  "channels": ["durov", "telegram"],
  "maxMessagesPerChannel": 20,
  "includeChannelInfo": true
}
```

### Output

One `message` row per exported message:

```json
{
  "type": "message",
  "channel": "durov",
  "message_id": 549,
  "url": "https://t.me/durov/549",
  "date": "2026-09-05T10:00:00+00:00",
  "text": "Something new happened today.",
  "views": 812000,
  "forwarded_from": null,
  "reply_to_id": null,
  "has_media": false,
  "media_type": null,
  "media_urls": [],
  "links": ["https://telegram.org"],
  "hashtags": [],
  "edited": false,
  "author": null
}
```

One `channel` row per channel, when `includeChannelInfo` is on:

```json
{
  "type": "channel",
  "channel": "durov",
  "title": "Pavel Durov",
  "username": "durov",
  "description": "Founder of Telegram.",
  "subscribers": 10600000,
  "verified": true,
  "photo": "https://cdn4.telesco.pe/file/....jpg",
  "messages_returned": 20,
  "checked_at": "2026-09-29T12:00:00.000Z"
}
```

Channels that fail get a free `channel` status row instead of message rows —
the run never charges for a channel it couldn't read:

```json
{
  "type": "channel",
  "input": "thischanneldoesnotexist9x7q",
  "channel": "thischanneldoesnotexist9x7q",
  "status": "not_found",
  "error": null,
  "checked_at": "2026-09-29T12:00:00.000Z"
}
```

`status` is one of:

- `not_found` — the username does not resolve to a channel.
- `private_or_no_preview` — the page shows Telegram's contact card but with
  channel content on it (see Limits below) — likely a channel that exists but
  whose message preview isn't public.
- `error` — the page failed to load after retries.
- `invalid` — the input entry is not a valid channel username.

The key-value store record `OUTPUT` holds a per-channel and per-run summary
(counts, HTTP stats, `stopped_reason`, `charged_events`). Every run leaves at
least one dataset row, even a run that matches nothing, so a quiet run is
never mistaken for a broken one.

### Pricing

Pay per event, nothing else:

- **$0.002 per channel checked** (only charged when `includeChannelInfo` is on
  and the channel loaded successfully).
- **$0.001 per message returned** ($1 per 1,000 messages).

Example: 3 channels, 200 messages each, with channel info = 3 × $0.002 +
600 × $0.001 = **$0.61**. If you set a maximum total charge for the
run, the actor stops cleanly once it's reached, delivering only what it
already charged for, and says so in `OUTPUT.stopped_reason`
(`"max_total_charge_reached"`).

### Limits, stated plainly

- **Public channels only.** The actor reads `https://t.me/s/<channel>` —
  exactly what a browser without a Telegram account sees. Private groups,
  member lists and phone numbers are never available this way.
- **`not_found` vs `private_or_no_preview` is a best-effort guess.** Telegram's
  logged-out contact-card page looks nearly identical whether a username is
  simply unregistered or belongs to a channel whose preview isn't public; the
  actor infers which from whether the page carries any channel content. Don't
  treat the distinction as authoritative.
- **No media download.** Photos, videos and documents only set `media_type`
  and, when the page's HTML happens to expose one (a photo/video thumbnail
  URL, a document link), `media_urls`. No files are fetched.
- **`reply_to_id` and `author` are best effort.** They come from the reply
  preview block and the "Sign messages" signature the page shows, when
  present; many channels show neither.
- **View counts are approximate by nature.** Telegram rounds the public
  counter (`2.67M`, `1.2K`); a re-run may show a slightly different number.
- **It stays polite:** at most 2 channels fetched at a time, at least 1.5 s
  between page requests of the same channel, HTTP 429 honoured (Retry-After,
  capped at 60 s, max 3 retries), a normal browser-like User-Agent, 20 s
  timeouts. A channel needs roughly 1 page per 20 messages, so exporting 10
  channels × 200 messages takes a couple of minutes.

### FAQ

**Can this scrape private channels or groups?** No. Only channels with a
public username and a public web preview.

**Does it get member lists or phone numbers?** No, never — the public preview
never exposes those.

**Can I get only messages from the last week?** Yes, set `since` to an ISO
date; pagination stops once it reaches older messages, so this is also
cheaper than exporting everything and filtering afterward.

**Can I filter by topic?** Yes, use `keywords` — case-insensitive substring
match against the message text.

**I need a daily digest of only what changed, not a full re-export.** Use our
companion actor, Telegram Channel Monitor – Change Feed, instead.

### Local development

```bash
pnpm --filter @mmnm/tgexport test        # unit tests, no network
pnpm --filter @mmnm/tgexport build
```

`node src/main.ts` runs the actor locally with Apify's local storage
(`./storage`); `ACTOR_TEST_PAY_PER_EVENT=true ACTOR_MAX_TOTAL_CHARGE_USD=1`
exercises the charging path with the SDK's $1 local test price.

# Actor input Schema

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

Public Telegram channels to export: usernames (durov), @durov, t.me/durov, or https://t.me/s/durov. Public channels only; private ones and non-existent names come back as a status row instead of messages. Up to 1,000 per run.

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

How many of the most recent messages to export per channel (1 to 10,000). Paginates backwards through the web preview.

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

Optional. Only export messages on or after this date/time (e.g. 2026-01-01 or 2026-01-01T00:00:00Z). Pagination stops once it reaches messages older than this.

## `until` (type: `string`):

Optional. Only export messages on or before this date/time.

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

Optional. Keep only messages whose text contains at least one of these keywords (case-insensitive substring match). Leave empty to keep every message.

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

Also return one row per channel with its title, description, subscriber count, photo URL and verified badge.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "telegram"
  ],
  "maxMessagesPerChannel": 20,
  "includeChannelInfo": true
}
```

# Actor output Schema

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

No description

## `summary` (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": [
        "durov",
        "telegram"
    ],
    "maxMessagesPerChannel": 20
};

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

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

```

## MCP server setup

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