A lot of public information appears on Telegram first. Exchanges post listings and delistings there, projects announce releases, and news channels push stories before the website updates. If you research a market or monitor a competitor, you will eventually want those posts in a table: one row per post, with the text, date, view count and links.
The official routes are awkward. The Bot API only sees channels where your bot is an admin. The full MTProto API (Telethon, Pyrogram) works, but needs a Telegram account with a phone number, an api_id from my.telegram.org and a session file to keep safe. That is a lot of setup for posts anyone can already read at t.me/s/<channel> in a browser.
This tutorial uses an Apify Actor that reads that public web preview and exports the posts as JSON, CSV or Excel.
Disclosure: I built and maintain this Actor. It is a paid tool ($1.00 per 1,000 results). The limitations section at the end is as important as the rest.
What you can do with the data
- Monitor announcement channels. Pull 30 days of an exchange's channel and tag each post: delisting, new listing, maintenance, promotion.
- Compare channels. Views per post, posting frequency and hours, and which sites they link to.
- Build a research dataset. Original-language text with dates and engagement, ready for topic or sentiment analysis.
Step 1: Set up a run in Apify Console
Open Telegram Channel Scraper and click Try for free. The input form has these fields:
| Field | What it does |
|---|---|
What to scrape (mode) |
posts (posts of channels), search (posts that contain a keyword) or channelInfo (one row per channel, with subscriber count and description). |
Channels (channels) |
Public channels as names (durov), @names (@durov) or links (https://t.me/durov). |
Search keyword (searchQuery) |
Only for search mode. |
Max results (maxItems) |
Upper limit for the whole run. You're only charged for saved results. |
Max posts per channel (maxItemsPerChannel) |
Stops one busy channel from using up the whole run. |
Only posts newer than (postedAfter) |
A date like 2026-10-01 or a period like 7 days. The Actor stops reading a channel when it reaches older posts. |
Single post links (postUrls) |
Optional links like https://t.me/telegram/105 if you only need specific posts. |
No Telegram account, phone number, API key or proxy is needed.
Step 2: Run it and export
Click Start. When the run finishes, open the Output tab and export as JSON, CSV or Excel.
For this article I ran this input, the newest 10 posts each from Binance's announcement channel and Telegram's own news channel:
{
"mode": "posts",
"channels": ["binance_announcements", "telegram"],
"maxItemsPerChannel": 10,
"maxItems": 20
}
It finished in about 4 seconds and saved 20 posts. Here are two of them. I removed the textHtml field, the media list (long file links) and most empty fields to keep this short; the values that remain are unchanged. Both are public brand channels, and none of these posts had an author signature, so there was nothing personal to redact:
[
{
"type": "post",
"channel": "binance_announcements",
"channelTitle": "Binance Announcements",
"messageId": 9089,
"url": "https://t.me/binance_announcements/9089",
"text": "Notice of Removal of Spot Trading Pairs - 2026-10-30\n\n\nBased on our most recent reviews, Binance will remove and cease trading on the following spot trading pairs:\n- At 2026-10-30 03:00 (UTC): AAVE/BTC, ATOM/BTC, CAKE/BTC, DASH/BTC, ETC/BTC, ICP/BTC, PENDLE/BTC, RENDER/BTC, TAO/BTC, THETA/BTC, XAUT/BTC and XTZ/BTC\n\n👉🏻 Learn more here",
"postedAt": "2026-10-27T08:31:38Z",
"views": 20700,
"isEdited": false,
"mediaTypes": ["photo"],
"mediaCount": 1,
"links": ["https://www.binance.com/en/support/announcement/detail/de45689cbd7b4e1fa87055cdf5a85997?utm_source=EnglishTelegram&utm_medium=GlobalCommunity&utm_campaign=AnnouncementBot"],
"hashtags": [],
"mentions": [],
"reactionsTotal": 93,
"reactions": [
{"emoji": "❤", "count": 35, "isPaid": false, "customEmojiId": null},
{"emoji": "🤔", "count": 26, "isPaid": false, "customEmojiId": null},
{"emoji": "👍", "count": 16, "isPaid": false, "customEmojiId": null},
{"emoji": "🤯", "count": 8, "isPaid": false, "customEmojiId": null},
{"emoji": "👏", "count": 5, "isPaid": false, "customEmojiId": null},
{"emoji": "🙏", "count": 3, "isPaid": false, "customEmojiId": null}
],
"scrapedAt": "2026-10-27T21:24:28Z"
},
{
"type": "post",
"channel": "telegram",
"channelTitle": "Telegram News",
"messageId": 453,
"url": "https://t.me/telegram/453",
"text": "350 Million GIFs. The GIF panel now contains over 350 million GIFs in 36 languages.\n\nYour Telegram GIF searches are entirely private and do not rely on any third parties.\n\nJuly Features\n1 • 2 • 3 • 4 • More",
"postedAt": "2026-07-19T17:58:17Z",
"views": 2020000,
"isEdited": false,
"mediaTypes": ["video"],
"mediaCount": 1,
"links": ["https://t.me/telegram/450", "https://t.me/telegram/451", "https://t.me/telegram/452", "https://t.me/telegram/454"],
"hashtags": [],
"mentions": [],
"reactionsTotal": null,
"reactions": [],
"scrapedAt": "2026-10-27T21:24:28Z"
}
]
Even 20 rows show a few patterns. The 10 Binance posts covered five days, and all of them went out within two minutes after the hour or half hour (07:02, 08:01, 08:31, 09:01 UTC and so on), which looks scheduled. The 10 Telegram posts came from two feature drops, in July and August, each posted as a burst of messages within a minute or two. Telegram's channel shows no reactions, so reactionsTotal is null there, not zero.
links holds every link in the post, including ones hidden behind text like "Learn more here".
Step 3: Do it from Python
Install the official client and pandas:
pip install apify-client pandas
Then pull the last 30 days of one channel and save a flat CSV:
from apify_client import ApifyClient
import pandas as pd
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("pipiagent/telegram-channel-scraper").call(run_input={
"mode": "posts",
"channels": ["binance_announcements"],
"postedAfter": "30 days",
"maxItems": 2000,
})
items = list(client.dataset(run.default_dataset_id).iterate_items())
df = pd.DataFrame(items)
# Flat columns for the CSV; nested lists like media and reactions are easier to use from JSON.
cols = ["channel", "messageId", "postedAt", "views", "reactionsTotal", "text", "url"]
df[cols].to_csv("binance_announcements_30d.csv", index=False)
Now tag and summarise the posts. Some posts have no text or no reactions, so the code handles missing values:
df["postedAt"] = pd.to_datetime(df["postedAt"], utc=True)
text = df["text"].fillna("").str.lower()
categories = {
"delisting": r"removal|delist|will remove",
"new listing": r"will list|new trading pairs|will support",
"maintenance": r"maintenance|upgrade",
"promotion": r"competition|campaign|rewards?|giveaway",
}
df["category"] = "other"
for name, pattern in categories.items():
df.loc[df["category"].eq("other") & text.str.contains(pattern), "category"] = name
summary = df.groupby("category").agg(
posts=("messageId", "count"),
median_views=("views", "median"),
median_reactions=("reactionsTotal", "median"),
).sort_values("posts", ascending=False)
print(summary)
# Posting hours (UTC)
print(df["postedAt"].dt.hour.value_counts().sort_index())
# Which sites the channel links to
domains = df["links"].explode().dropna().str.extract(r"https?://([^/?]+)")[0]
print(domains.value_counts().head(10))
The patterns are a starting point for this one channel; adjust them after reading a few dozen posts.
To monitor a channel, schedule the same input in Apify with "postedAfter": "1 day" and append each run's results to your table. De-duplicate on channel + messageId, because runs can overlap.
What it costs
You pay per saved result: $1.00 per 1,000 results, with no start fee and no monthly rental. A post with an album of ten photos is still one result.
- The 20-post test above: $0.02
- 30 days of a channel that posts about 10 times a day (around 300 posts): about $0.30
- Channel info (subscriber count, description) for 50 channels: $0.05
You can set a maximum charge per run in the run options, and the Actor stops when it reaches it.
Limitations
These mostly come from what Telegram shows on its public web preview:
-
Public channels only. Private channels, invite links (
t.me/+...), groups, bots and user accounts can't be read. They are skipped with a warning in the log. - Some public channels have no web preview. Owners can turn it off, Telegram hides some restricted channels, and a channel blocked in the country of Apify's servers looks like it doesn't exist (a proxy can help).
- No comments. Comments under posts aren't part of the web preview.
- Views and reactions are rounded by Telegram above 1,000 ("20.7K" becomes 20700), so small differences between posts don't mean much.
- Views keep growing after a post goes out. In my sample, Binance posts from four days earlier had more than twice the views of the newest ones. Compare posts of similar age, or re-run later.
-
Some post types show no content on the web. Live streams, giveaways and paid media come back with empty text and a
mediaNote. - Media links expire. The Actor saves links to photos and videos, not the files. Download them soon if you need them.
- Search returns at most about 22 posts per channel and keyword. For more, scrape the channel's posts and filter them yourself, as in the example above.
- Old posts take time. The Actor reads newest to oldest at roughly 1,000 posts a minute.
- Personal data. Author signatures and forwarded-from names can identify people. If you store them, make sure you have a legitimate reason under GDPR or similar laws.
If you track many channels, run channelInfo mode daily to build a subscriber history.
The store page has more input examples. If something doesn't match what this article says, open an issue there and I'll look into it.
Top comments (0)