# Telegram Channel Scraper (`rjmacarthy/telegram-channel-scraper`) Actor

Scrape posts from public Telegram channels: text, date, views, reactions, photos, videos, forwards, replies, links and hashtags, plus channel info and subscriber counts. Date and keyword filters, incremental "only new posts" mode. No login, phone number or API keys.

- **URL**: https://apify.com/rjmacarthy/telegram-channel-scraper.md
- **Developed by:** [rj](https://apify.com/rjmacarthy) (community)
- **Categories:** Social media, Automation, 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 post scrapeds

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: Extract Posts, Views, Reactions and Media from Public Telegram Channels (No Login)

**Telegram Channel Scraper** downloads posts from any **public Telegram channel** and returns clean, structured data: text, publish date, view count, reactions, photos, videos with durations, forwards, replies, links, hashtags and mentions. It can also return **channel info**: title, description, subscriber count and photo.

It reads Telegram's own public web preview, `t.me/s/<channel>`, the page anyone can open in a browser. You need **no Telegram account, no phone number and no API keys**, and you **only pay for the posts you get**.

Use it to monitor news and crypto channels, archive a channel, track how many views and reactions a channel gets, or feed posts into alerts, spreadsheets and AI pipelines.

### What can this Telegram scraper do?

- 📢 **Scrape any public channel.** Paste usernames (`durov`), `@names` or links (`https://t.me/durov`, `https://t.me/s/durov`, or a post link like `https://t.me/durov/531`). Add as many channels as you like.
- 📝 **Complete post data.** You get plain text (line breaks and emoji kept) and, optionally, HTML. Each post also has its exact UTC date, views as a number ("1.85M" becomes 1850000), the author signature, an edited flag, forwarded-from, the post it replies to, link previews, links, hashtags, mentions and reactions with counts.
- 🖼️ **Media details.** Each attachment has a type (photo, video, GIF and others), photo URLs, video thumbnails and video durations in seconds. Video file URLs are included when Telegram embeds the video.
- 🗓️ **Filter by date and keyword.** Get only posts newer than a date (`2026-10-01` or `7 days`) and/or posts containing a keyword. Paging stops at the date limit, so old history isn't downloaded for nothing.
- 🔁 **Incremental monitoring.** Turn on **Only new posts** and schedule the Actor. Each run outputs, and charges for, only posts published since the previous run. A run with nothing new costs one request per channel.
- 🛡️ **Polite and reliable.** It makes one request per second by default and slows down on its own if Telegram rate-limits. Each channel is handled separately: a missing, private or misspelled channel produces a clear warning, and the other channels continue.
- 💸 **Pay per post.** There's no subscription. Channel-info items are free.

### Why this scraper?

| | **Telegram Channel Scraper** |
|---|---|
| Login, phone number, Telegram API ID | ❌ Not needed |
| Price | $0.50 per 1,000 posts. Channel info is free |
| Views as numbers, reactions with counts | ✅ |
| Photos, videos (duration, thumbnail), GIFs | ✅ |
| Forwarded-from, reply-to, edited flag | ✅ |
| Links, hashtags, mentions, link previews | ✅ |
| Date filter that stops paging early | ✅ |
| Keyword filter (non-matching posts are free) | ✅ |
| Incremental "only new posts" mode | ✅ Per channel, per monitor |
| Missing or private channel kills the run | ❌ Reported with a clear reason; other channels continue |
| Proxy required | ❌ Off by default |

### How to scrape a Telegram channel

1. Click **Try for free** or **Start**.
2. Add one or more channels to **Channels**: `durov`, `@telegram` or `https://t.me/tginfo`.
3. Optionally set **Max posts per channel**, **Only posts newer than** or **Keywords**.
4. Click **Start**. Download the posts as JSON, CSV, Excel or HTML, or get them through the API.

**Will a channel work?** Open `https://t.me/s/CHANNELNAME` in a browser. If you see the channel's posts, this scraper can read them. If the browser jumps to `https://t.me/CHANNELNAME` instead, the channel has no public web preview, and the scraper reports it as such.

### Input example

```json
{
    "channels": ["durov", "@telegram", "https://t.me/tginfo"],
    "maxPostsPerChannel": 100,
    "onlyPostsNewerThan": "30 days",
    "keywords": [],
    "includeChannelInfo": true,
    "includeTextHtml": false,
    "onlyNewPosts": false
}
```

| Field | Description |
|---|---|
| `channels` | Public channel usernames or t.me links (required). Invite links (`t.me/+...`), groups, users and bots are skipped with a warning. |
| `maxPostsPerChannel` | Limit per channel, newest first. `0` means no limit. Default `100`. |
| `onlyPostsNewerThan` | Only posts published after this date: `2026-10-01`, `2026-10-01T08:00:00Z`, or relative such as `7 days`, `2 weeks`, `24 hours`. |
| `keywords` | Only posts whose text contains any of these words or phrases (case-insensitive). Non-matching posts aren't charged. |
| `includeChannelInfo` | One extra `channel-info` item per channel (default `true`, free). |
| `includeTextHtml` | Also return the post text as HTML in `textHtml` (default `false`). |
| `onlyNewPosts` | Incremental mode: only posts published since the last run. See below. |
| `stateKey` | Name of the monitor. Separate monitors keep separate memories. |
| `maxPagesPerChannel` | Safety limit on pages per channel (20 posts each). `0` means automatic: no limit, except that a keyword filter without a date limit stops after 100 pages, about 2,000 posts. |
| `requestDelayMs` | Minimum time between requests to t.me. Default 1,000 ms. |
| `proxyConfiguration` | Off by default. Turn it on only if t.me rate-limits your runs. |

### Output example

Every post is one dataset item with `type: "post"`:

```json
{
    "type": "post",
    "channel": "telegram",
    "channelTitle": "Telegram News",
    "postId": 459,
    "url": "https://t.me/telegram/459",
    "date": "2026-08-26T19:12:24.000Z",
    "text": "Signed Gifts. You can now add your signature and a comment to gifts you buy via the gift marketplace.\n\nAugust Features\n1 • 2 • 3 • 4 • 5 • More",
    "views": 2000000,
    "author": null,
    "isEdited": false,
    "isForwarded": false,
    "forwardedFrom": null,
    "forwardedFromUrl": null,
    "replyToPostId": null,
    "media": [
        {
            "type": "video",
            "url": "https://cdn1.telesco.pe/file/6efb59a075.mp4?token=...",
            "thumbnailUrl": "https://cdn1.telesco.pe/file/jF1dZ87x...",
            "duration": 12
        }
    ],
    "linkPreview": null,
    "links": ["https://t.me/telegram/455", "https://t.me/telegram/456"],
    "hashtags": [],
    "mentions": [],
    "reactions": [],
    "scrapedAt": "2026-11-02T11:04:17.695Z"
}
```

With **Include channel info** on, each channel also gets one `channel-info` item:

```json
{
    "type": "channel-info",
    "channel": "durov",
    "channelTitle": "Pavel Durov",
    "url": "https://t.me/durov",
    "description": "Founder of Telegram.",
    "subscribers": 10200000,
    "counters": { "photos": 102, "videos": 47, "links": 200 },
    "isVerified": true,
    "photoUrl": "https://cdn4.telesco.pe/file/AOgkeE7H....jpg",
    "scrapedAt": "2026-11-02T11:04:15.977Z"
}
```

More real items are in the Actor's sample output. Notes on the fields:

- `type` tells posts and channel-info items apart. Filter on it when you export.
- `postId` grows with every message in the channel. Some numbers are missing because of deleted posts, albums and service messages. `channel` + `postId` is a stable unique key.
- `views` and `subscribers` are numbers parsed from Telegram's rounded display ("12.3K" becomes 12300, "10.2M" becomes 10200000). They are approximate for large values.
- `media[].type` is `photo`, `video`, `gif`, `round_video`, `voice`, `document`, `sticker`, `poll`, `location`, or `other` for anything not recognised. A video that Telegram doesn't embed ("Media is too big") has `url: null`, `notEmbedded: true` and still a thumbnail and duration.
- `reactions[]` has `type` (`emoji`, `custom_emoji` or `paid` for Telegram Stars), `emoji`, `customEmojiId` and `count`. Custom emoji have no standard character, so only the ID is given.
- `forwardedFrom` is `null` when Telegram hides the original sender's name. `isForwarded` is still `true`.
- Media URLs point to Telegram's CDN (`telesco.pe`). Video URLs carry an access token, so download files you need soon after the run.

A `RUN_SUMMARY` record in the run's key-value store lists, for every channel: status, posts output, posts scanned, pages fetched, filtered posts, why it stopped, and errors.

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

This Actor uses **pay-per-event** pricing. You pay **$0.50 per 1,000 posts** ($0.0005 per post) for posts that appear in your dataset. These cost nothing:

- channel-info items
- posts removed by your keyword or date filters
- already-seen posts in incremental mode
- missing or private channels

| Posts | Price |
|---|---|
| 100 | $0.05 |
| 1,000 | $0.50 |
| 10,000 | $5.00 |

Your Apify plan's free monthly credit applies. To cap spending, set **Maximum cost per run** in the run options. The scraper stops cleanly when the budget is reached and says so in the status message.

### Scheduling and incremental monitoring of new posts

Use this to get **alerts for new posts** in specific channels, or to keep an archive in sync:

1. Turn on **Only new posts** and give the monitor a name in **Monitor name** (e.g. `crypto-news`).
2. Save the configuration as a **Task** and add a **Schedule** (e.g. every hour).
3. The first run outputs posts as usual (respecting your limits) and remembers the newest post of each channel.
4. Every later run asks Telegram only for posts **after** that one, so it outputs only what is new. New posts arrive oldest first.

If more new posts arrived than **Max posts per channel** (or your budget) allows, the run outputs the oldest ones and remembers where it stopped, and the rest arrive in the next run. Nothing is skipped and nothing is charged twice.

The memory is kept per channel in a named key-value store (`telegram-channel-scraper-state` by default), under the monitor name you choose. Different monitor names are fully independent. Don't run two runs with the same monitor name at the same time.

### Integrations and API

- **API.** Start runs and fetch results with the [Apify API](https://docs.apify.com/api/v2), or the JavaScript and Python clients:

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

- **Webhooks.** Get notified (or trigger your pipeline) when a run finishes.

- **Zapier, Make, n8n.** Send new posts to Google Sheets, Airtable, Slack, Discord, email or your CRM with no code.

- **Google Sheets, Slack, Airbyte, Keboola and more** through Apify integrations.

- **AI agents.** Use the Actor as a tool through the Apify MCP server.

### FAQ

**Do I need a Telegram account, phone number or API ID?**
No. The scraper reads the public web preview at `t.me/s/<channel>`, the same page anyone can open in a browser without logging in.

**Which channels can I scrape?**
Public channels that have a username and a web preview. Private channels, invite-only channels, groups, users and bots aren't supported. A few public channels have the web preview turned off; the run reports them as "no public preview".

**What happens with a channel that doesn't exist or is private?**
It's reported with a clear reason, such as "No public Telegram channel with this username exists" or "This username belongs to a bot, not a channel", and the other channels continue. You're not charged for it.

**How fast is it?**
Each page holds up to 20 posts, and the scraper fetches one page per second by default. In testing, 30 posts from each of 3 channels (90 posts plus channel info) took 6 requests; the whole run, including two usernames that turned out not to be channels, took 9 seconds. Expect about 1,000 posts per minute, a bit less for channels that post many albums (40 posts from a busy news channel took 3 pages).

**Can I get the whole history of a channel?**
Yes. Set **Max posts per channel** to `0` and leave the date empty. Large channels have hundreds of thousands of posts, so consider a date limit.

**Does it return comments?**
No. Comments live in a separate discussion group, which the web preview doesn't show.

**Why is `text` empty for some posts?**
Some posts are only a photo, video or sticker without a caption. The attachment is still in `media`.

**Is scraping Telegram channels legal?**
The scraper only collects posts that channel owners publish openly on the web. It doesn't collect member lists, phone numbers or private messages. Always check that your use complies with the applicable terms and laws, especially for personal data in post text.

### Limits

- Public channel posts and channel metadata only. No member lists, user lookups, private groups or comments.
- Views, reactions and subscriber counts are rounded by Telegram ("12.3K").
- Rare post types (polls, documents, voice messages, stickers, locations) are parsed from the web preview on a best-effort basis. Anything unrecognised is reported as `{ "type": "other" }` instead of failing the run.
- The keyword filter matches the post text only (not link previews or file names).
- The reason given for a channel without a preview (not found, bot, group, user, preview disabled) is a best-effort reading of Telegram's profile page.
- Incremental state lives in a named key-value store in your account, created by the Actor. Deleting that store resets the monitor.

### Ready-to-run examples

- [Monitor crypto news Telegram channels](https://apify.com/rjmacarthy/telegram-channel-scraper/examples/crypto-news-telegram-channels)
- [Track keywords in Bloomberg and NYT Telegram channels](https://apify.com/rjmacarthy/telegram-channel-scraper/examples/news-telegram-channels-keyword-alerts)

### More scrapers by the same developer

- [Substack Scraper](https://apify.com/rjmacarthy/substack-scraper)
- [ATS Jobs Scraper: Greenhouse, Lever, Ashby & Workday](https://apify.com/rjmacarthy/ats-jobs-scraper)
- [Workday Jobs Scraper](https://apify.com/rjmacarthy/workday-jobs-scraper)
- [Google Ads Transparency Scraper](https://apify.com/rjmacarthy/google-ads-transparency-scraper)
- [Dice Jobs Scraper](https://apify.com/rjmacarthy/dice-jobs-scraper)
- [SEEK Jobs Scraper](https://apify.com/rjmacarthy/seek-jobs-scraper)

Browse all my Actors on [my Apify Store profile](https://apify.com/rjmacarthy).

### Support

Found a channel that doesn't work, or need an extra field? Open an issue on the **Issues** tab with the channel name. Fixes are usually quick.

# Actor input Schema

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

Public channel usernames or links: durov, @durov, https://t.me/durov, https://t.me/s/durov or a post link such as https://t.me/durov/531. Private invite links (t.me/+...), groups, users and bots are not supported.

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

Stop after this many posts from each channel (newest first). 0 = no limit (the whole channel history, or everything newer than the date below).

## `onlyPostsNewerThan` (type: `string`):

Only posts published after this date. Use an absolute date (2026-10-01) or a relative one (7 days, 2 weeks, 24 hours). Leave empty for no date limit.

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

Only output posts whose text contains at least one of these words or phrases (case-insensitive). Posts that don't match are not charged. Leave empty for all posts.

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

Add one extra item per channel (type "channel-info") with the title, description, subscriber count, photo and media counters.

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

Also output the post text as HTML (textHtml), with bold, links and other formatting kept. The plain-text field (text) is always included.

## `onlyNewPosts` (type: `boolean`):

Remember the newest post seen in each channel and, on later runs, output (and charge for) only posts published since. The first run outputs posts as usual and saves the newest one as the starting point.

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

Name of this monitor's memory. Runs with the same name share the "last seen post" of each channel. Use different names for independent monitors.

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

Named key-value store in your account where incremental state is kept (letters, digits and dashes).

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

Safety limit on pages fetched per channel (each page has up to 20 posts). 0 = automatic: no limit, except when a keyword filter is used without a date limit, where scanning stops after 100 pages (about 2,000 posts).

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

Minimum time between two requests to t.me. The default (1 second) is polite and avoids rate limits. If t.me answers with HTTP 429, the scraper slows down further on its own.

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

Not needed for most runs: t.me previews are public. Turn it on only if t.me rate-limits or blocks the run (for example very large runs).

## Actor input object example

```json
{
  "channels": [
    "durov",
    "telegram"
  ],
  "maxPostsPerChannel": 20,
  "includeChannelInfo": true,
  "includeTextHtml": false,
  "onlyNewPosts": false,
  "stateKey": "default",
  "stateStoreName": "telegram-channel-scraper-state",
  "maxPagesPerChannel": 0,
  "requestDelayMs": 1000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One item per post: channel, date, text, views, forward/reply info and link.

## `postsFull` (type: `string`):

Every field, including media, link previews, links, hashtags, reactions and channel-info items.

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

Per-channel status, counts, stop reason and any warnings (missing or private channels).

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

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

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

```

## MCP server setup

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