# Telegram Channel Messages Scraper (`lightmoon/telegram-messages-scraper`) Actor

Export public Telegram channel posts to CSV or JSON — text, views, reactions, media links and dates. From $0.99 per 1,000 messages. No account, no API key and no phone number. Search by keyword, collect only new posts, monitor news and crypto channels, track brand mentions or archive a channel.

- **URL**: https://apify.com/lightmoon/telegram-messages-scraper.md
- **Developed by:** [Stable](https://apify.com/lightmoon) (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

from $0.99 / 1,000 channel messages

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

Export posts from public Telegram channels — text, date, views, reactions, media
links, forwards, replies and link previews — to CSV, Excel or JSON. **No Telegram
account, no `api_id`, no phone number, no bot token.**

Two things worth knowing before you read further. **Telegram runs the keyword
search itself**, so looking for one brand in a channel with 50 000 posts costs a
few requests instead of two and a half thousand — and you are billed for the
posts that match, not for the archive. And **a repeat run collects only what is
new**, walking forward from the last post it saw, so a channel you already have
is never downloaded or billed twice.

**From $0.99 per 1,000 messages**, billed once per stored message. A post a
filter removed, a post reached twice in one run, and a scheduled run that finds
nothing new all cost nothing. Public channels only — groups and private links
need an account this actor does not use.

```json
{ "channels": ["durov", "t.me/bloomberg", "@breakingmash"], "maxPosts": 100 }
```

### What one row looks like

Real output, collected 2026-08-18 from a post published the day before. Two
edits for width and nothing else: the media URLs are truncated, and the columns
that came back empty on this particular post are left out.

```json
{
  "channel": "cointelegraph",
  "messageId": 71658,
  "url": "https://t.me/cointelegraph/71658",
  "date": "2026-08-17T12:08:28+00:00",
  "text": "🚨 JUST IN: Strategy raised $333.7M through stock sales last week but bought no Bitcoin.\n\nNews | Markets | YouTube",
  "textLength": 113,
  "views": 7030,
  "viewsText": "7.03K",
  "viewsAreExact": false,
  "reactionsTotal": 49,
  "reactionTypes": 3,
  "reactions": [
    { "emoji": "❤", "count": 24, "exact": true, "paid": false },
    { "emoji": "🤡", "count": 14, "exact": true, "paid": false },
    { "emoji": "🔥", "count": 11, "exact": true, "paid": false }
  ],
  "isEdited": false,
  "isForwarded": false,
  "isReply": false,
  "hasMedia": true,
  "isAlbum": true,
  "photoCount": 2,
  "photos": [
    "https://cdn5.telesco.pe/file/VYx-WLzNEWcAJ9IVQ…",
    "https://cdn5.telesco.pe/file/HP7Su0DnsROPe5cvP…"
  ],
  "videoCount": 0,
  "documentCount": 0,
  "links": [
    "https://cointelegraph.com/",
    "https://cointelegraph.com/category/markets",
    "https://www.youtube.com/@cointelegraph"
  ],
  "hashtags": [],
  "mentions": [],
  "channelTitle": "Cointelegraph",
  "channelSubscribers": 340875,
  "channelSubscribersText": "340 875 subscribers",
  "channelSubscribersAreExact": true,
  "channelPhotoCount": 19900,
  "channelVideoCount": 1270,
  "channelLinkCount": 57700,
  "scrapedAt": "2026-08-18T03:00:17+00:00"
}
```

Three columns in that row are worth pointing at, because they are the ones a
buyer usually has to find out the hard way.

**`viewsAreExact`.** Telegram publishes large counters rounded — `7.03K` views,
a `55.1K` reaction — and there is no unrounded copy anywhere on the page it
serves. Counts under a thousand are exact, and this column tells you which of the
two you are holding, so an average over a column means something.

**`isAlbum`.** Two photos under one post, and Telegram counts them as two
messages internally. Rows are still one per post, and you are billed once.

**`channelSubscribersAreExact`.** The channel page rounds the subscriber count to
`341K`. Switch on **Add the exact subscriber count** and the actor also reads the
channel's profile page for the real number — one extra request per channel, never
billed.

### Fields

Up to **52 columns** per row. Empty ones are dropped, so a text-only post has no
media columns and a CSV of one channel has no columns of blanks.

| Column | What it is |
|---|---|
| `channel`, `channelUrl` | the channel this post is from |
| `messageId`, `url` | the post's number and its permanent link |
| `date` | full ISO 8601 timestamp with time zone |
| `text`, `textLength` | the post as plain text, line breaks intact |
| `textHtml` | the same with bold, italics and links — optional |
| `views`, `viewsText`, `viewsAreExact` | the number, the string Telegram printed, and whether it is exact |
| `reactionsTotal`, `reactionTypes`, `reactions` | totals, plus every emoji with its own count |
| `authorSignature` | the author's name, on channels that sign posts |
| `isEdited` | the post was edited after publishing |
| `isForwarded`, `forwardedFrom`, `forwardedFromUrl` | where a forwarded post came from |
| `isReply`, `replyToUrl`, `replyToText` | the post being answered |
| `hasMedia`, `isAlbum` | what is attached, and whether it is one album |
| `photoCount`, `photos` | direct image URLs |
| `videoCount`, `videos`, `videosWithFileUrl` | thumbnail for every video, plus a playable file URL for most — 71 of 82 measured carried one, and the count says how many you can actually fetch |
| `documentCount`, `documents` | file title and kind |
| `linkPreviewUrl`, `linkPreviewSite`, `linkPreviewTitle`, `linkPreviewDescription`, `linkPreviewImage` | the article card under a shared link |
| `links`, `hashtags`, `mentions` | taken from the post's own markup, not guessed from the prose |
| `channelTitle`, `channelDescription`, `channelIsVerified`, `channelAvatar` | on every row, at no extra cost |
| `channelSubscribers`, `channelSubscribersText`, `channelSubscribersAreExact` | subscriber count |
| `channelPhotoCount`, `channelVideoCount`, `channelLinkCount`, `channelFileCount` | the channel's own totals |
| `matchedQuery`, `scrapedAt` | which keyword found this post, and when |

Five ready-made views — **Posts**, **Engagement**, **Media links**, **Links and
mentions**, **Channel details** — are on the Output tab, so a CSV of nine columns
is one click rather than a spreadsheet edit.

### Search inside a channel, and pay only for what matches

`searchQuery` is handed to Telegram, which filters its own archive server-side
and returns only the posts that match — paged, so a common word returns all of
its matches rather than the first twenty. Nothing that does not match is
downloaded, stored or billed.

```json
{ "channels": ["durov"], "searchQuery": "Apple", "maxPosts": 50 }
```

### Collect only what is new

Switch on `onlyNewSinceLastRun` and each channel gets its own resume point,
stored in your account and carried between runs. The next run reads **forwards**
from the last post it saw, so nothing is skipped even when a channel publishes
hundreds of posts between runs — and nothing you already have is downloaded or
billed again.

A scheduled run that finds nothing new **finishes green and charges you nothing**
— zero billable rows, because there were no messages to bill for. Measured on two
channels: two requests, under three seconds.

For one channel you can name the point yourself: `afterPostId: 3215` reads
everything published after `t.me/<channel>/3215`, oldest first.

### Filters

Every filter runs **before** a row is stored or billed, so a filtered-out post
costs nothing.

| Input | What it does |
|---|---|
| `searchQuery` | Telegram filters its own archive by keyword |
| `oldestPostDate` | stop as soon as posts are older than this |
| `newestPostDate` | skip posts newer than this |
| `onlyWithMedia` | photos, video or files only |
| `onlyWithLinks` | posts that link somewhere, including shared-article cards |
| `minViews` | a floor on the view count |
| `maxPosts`, `maxPostsPerChannel` | the total, and how deep to read one channel |

A post that carries no view count at all is dropped by `minViews` and counted
separately in the run summary, because it cannot be shown to clear the bar.

### Recipes

- **Media monitoring.** `channels` = the outlets you track, `searchQuery` = your
  brand, `onlyNewSinceLastRun` = on, run hourly. You pay for mentions.
- **Crypto and signal channels.** `channels` = the rooms you follow,
  `oldestPostDate` = this morning, `minViews` = 1000.
- **Archiving a channel before it disappears.** `maxPostsPerChannel: 0` and let
  it walk to the beginning; resume a long backfill with `afterPostId`.
- **Content research.** One competitor channel, `Engagement` view, sort by
  `reactionsTotal` — which posts actually landed.
- **Dataset building.** `includeTextHtml` keeps the formatting; `links` and
  `hashtags` come out as their own columns for labelling.

### What it costs

**From $0.99 per 1,000 messages**, billed once per stored message — and the rate
never rises above $2.00 whatever plan you are on. One price covers the whole row:
the text, the reactions, the media links, the link preview, the channel details
and the keyword search are all included — there is no second event on top.

- Nothing is charged for a post a filter removed.
- One post reached twice in a run — by an ordinary read and by a keyword search
  — is stored and billed **once**.
- A run that finds nothing new charges nothing at all.

**You are charged for results, not for machine time.** Compute, traffic and
storage for your run are not added to your bill — under pay-per-event the rate on
the card is the whole price.

**Apify's free plan** includes $5 of credit a month, and that credit is what pays
for results here — about **2 500 messages a month** at the free-plan rate. There
is no separate free trial: you are billed per post from the first post, so start
with `maxPosts: 20`, look at the output, then scale.

**Speed:** 300 posts in **65 seconds** on 256 MB — the smallest size this actor
runs in — using 15 requests and 2.1 MB of traffic.

### Limits

Stated up front rather than left to be discovered.

- **Public channels only.** Groups, private channels and invite links
  (`t.me/+…`) need a real Telegram account, which this actor does not use.
- **A channel owner can switch the web preview off**, and a channel that has
  done so cannot be read. Telegram serves the same page for a switched-off
  channel, a group and a username that does not exist, so the error says exactly
  that instead of guessing. Of twenty channel names tried on 2026-08-18,
  nineteen returned posts.
- **Large counters are rounded by Telegram** — views and reactions above a
  thousand. `viewsAreExact` and the `exact` flag on each reaction say so per row.
- **Custom (premium) emoji reactions carry an ID and no character**, because the
  character is not on the page at all. Standard emoji come through as emoji.
- **Comments are not collected.** They live in a channel's linked discussion
  group, which is a separate fetch per post.
- **Related-channel discovery is not offered** — the page Telegram serves does
  not contain it. Checked across 356 posts from 19 channels.
- **Media are links, not files.** Every photo, video and document comes back as
  a URL you can fetch yourself; nothing is downloaded into storage you pay for.
- **Not seen in a 356-post sample:** polls, voice notes, audio files, round
  video messages, stickers, locations and contacts. They are not claimed as
  supported.

### Output

Dataset rows, exportable as CSV, Excel, JSON, XML or RSS, plus a `RUN_SUMMARY`
in the key-value store with what each channel did: posts stored, pages fetched,
which names were not readable channels, which filter dropped what, duplicates
skipped without being billed, the resume point saved per channel, traffic used,
and whether Telegram slowed the run down at any point.

### FAQ

#### Do I need a Telegram account, API key or phone number?

No. This reads the public web preview Telegram itself publishes for channels
that have it switched on — the page behind the "Preview channel" button. There
is no login, no `api_id`, no `api_hash`, no session string and no bot token, and
none of those will ever appear in the input.

#### Will my run wait behind somebody else's?

No. Nothing is shared between users of this actor — no pooled account, no
session, no queue. Your run starts when you start it.

#### Can I scrape a private channel or a group?

No. Those need a real Telegram account. If you paste an invite link the actor
says so plainly rather than reporting the channel as missing.

#### How far back can it go?

To the beginning of what the channel publishes. Set `maxPostsPerChannel: 0` and
it walks the archive until Telegram stops offering an earlier page.

#### How much does it cost to export 10 000 posts?

**$9.90 at the card rate**, or $20 on the free and starter rate — and nothing
else, because the compute and traffic the run uses are not added to your bill. It takes about **36 minutes** and 70 MB, on the
evidence of a 300-post run that took 65 seconds. Use `maxPosts` to cap a run
before you start it.

#### Why did a channel return fewer posts than I asked for?

Telegram serves a window of about twenty **message IDs** per request, and an
album of nine photos occupies nine IDs while showing as one post. So a channel
that posts albums yields fewer posts per request than one that posts text. The
actor counts real posts and keeps paging; it never assumes twenty per page.

#### Why is a view count zero or missing?

Telegram does not publish one for every post. A missing count is left empty
rather than written as zero, because zero is a number people sort and filter on.

#### Can it watch channels on a schedule?

Yes. Switch on `onlyNewSinceLastRun` and put the actor on an Apify schedule.
Each run collects and bills only what appeared since the last one, and a run
that finds nothing new finishes successfully.

### Other scrapers we maintain

Same engine, same pricing model, same promise: flat rows, stated limits,
and no charge for a row that came back empty.

| | |
|---|---|
| **[Google Maps Contact Scraper](https://apify.com/lightmoon/google-maps-contact-scraper)** | Local businesses by category and city — phone, website, split address and opening hours. |
| **[Local Business Phone Scraper](https://apify.com/lightmoon/local-business-phone-scraper)** | The same job from directory sources, with Better Business Bureau accreditation on the row. |
| **[YouTube Influencer Scraper](https://apify.com/lightmoon/youtube-influencer-scraper)** | Creators by niche, with the website and socials they publish and a subscriber filter. |
| **[Threads Hashtag Scraper](https://apify.com/lightmoon/threads-hashtag-scraper)** | Threads posts by hashtag or keyword, with author, engagement and links. |
| **[RedNote Xiaohongshu Profile Scraper](https://apify.com/lightmoon/rednote-profile-scraper)** | Public RedNote creator profiles in bulk — followers, bio, IP location and their notes. |
| **[ATS Jobs Scraper](https://apify.com/lightmoon/ats-jobs-scraper)** | Open roles straight from company career pages on Greenhouse, Lever, Ashby and Workday. |

# Actor input Schema

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

One per line, in any form: `durov`, `@durov`, `t.me/durov`, `https://t.me/s/durov`, or a link to a single post. Groups and private invite links (t.me/+...) are not public channels and cannot be read.

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

A word or phrase. **Telegram runs this search itself**, so only matching posts are fetched and only matching posts are billed — a brand mention in a 50 000-post channel costs a few requests, not two and a half thousand. Results are paged, so a common word returns all of its matches and not just the first twenty. Leave empty to read the channel in order.

## `maxPosts` (type: `integer`):

Across all channels. 0 means no limit. Free-plan runs are capped by your own Apify usage, so start small.

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

How deep to read one channel before moving to the next. 0 means no limit. Telegram serves about twenty message IDs per request, and an album counts as several IDs but one post — so a channel that posts photo albums needs more requests per post than one that posts text.

## `oldestPostDate` (type: `string`):

`2026-08-01`, or a full timestamp. Reading stops as soon as a post older than this appears, so a narrow window costs a few requests rather than the whole archive.

## `newestPostDate` (type: `string`):

`2026-08-15`, or a full timestamp. Use with the field above to take one week out of the middle of an archive.

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

Remembers the last post ID it read for each channel and starts from there next time, so a scheduled run collects — and bills — only what is genuinely new. It reads forwards from where it stopped, so nothing is skipped even when a channel posts hundreds of times between runs. The first run has nothing to resume from and reads normally.

## `afterPostId` (type: `integer`):

The number at the end of a post link — `t.me/durov/523` is 523. Reads everything published after it, oldest first. Applies to a single channel; with several channels use the switch above, which keeps a separate resume point for each.

## `onlyWithMedia` (type: `boolean`):

Filters run before anything is stored or billed, so a filtered-out post costs you nothing.

## `onlyWithLinks` (type: `boolean`):

Counts both links inside the text and a shared article's link preview.

## `minViews` (type: `integer`):

Telegram rounds large view counts to `12.5M`, so this compares against that rounded number. A post with no view count at all is dropped by this filter and reported separately, because it cannot be shown to clear the bar.

## `includeChannelDetails` (type: `boolean`):

The channel's name, description, verified badge, avatar and its counts of photos, videos and links are on **every row already**, at no extra cost. Switch this on and the actor also reads the channel's profile page — one extra 11 KB request per channel, never billed — for the subscriber count as an exact number, `11 170 139`, instead of the `11.2M` Telegram rounds it to.

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

The post's HTML, with bold, italics and links intact. Roughly doubles the size of a row, so it is off unless you need the formatting.

## Actor input object example

```json
{
  "channels": [
    "durov"
  ],
  "searchQuery": "bitcoin",
  "maxPosts": 200,
  "maxPostsPerChannel": 1000,
  "oldestPostDate": "2026-08-01",
  "newestPostDate": "2026-08-15",
  "onlyNewSinceLastRun": false,
  "afterPostId": 523,
  "onlyWithMedia": false,
  "onlyWithLinks": false,
  "includeChannelDetails": false,
  "includeTextHtml": false
}
```

# Actor output Schema

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

No description

## `engagement` (type: `string`):

No description

## `media` (type: `string`):

No description

## `links` (type: `string`):

No description

## `channelInfo` (type: `string`):

No description

## `all` (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"
    ]
};

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

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

```

## MCP server setup

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