Telegram's official API needs an api_id, an api_hash and a phone number tied to a real account — a lot of ceremony (and risk) if all you want is the posts of a few public channels. This post shows a lighter route: the public web preview that Telegram publishes for every open channel at t.me/s/<channel>, packaged as an API call.
What you get
One JSON object per post: channel, channelTitle, channelSubscribers, id, url, date (UTC), text, author, views, reactions (per emoji) and reactionsTotal, forwardedFrom, replyTo, mediaTypes, photos, videos, documents, poll, linkPreview, links, hashtags, mentions. Newest first, with a date cut-off so daily runs stay tiny.
1. One curl
Create a free account at apify.com (no card; $5 of usage per month included), grab the API token from Settings → Integrations, then:
curl -X POST "https://api.apify.com/v2/acts/chorelet~telegram-channel-posts-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN&format=csv" \
-H "Content-Type: application/json" \
-d '{"channels": ["telegram", "durov"], "maxPostsPerChannel": 100, "postedAfter": "30 days"}' \
-o posts.csv
format=csv → spreadsheet; drop it for JSON; xlsx also works. The call blocks until the run finishes (a few seconds for a couple of hundred posts).
2. Python
pip install apify-client
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("chorelet/telegram-channel-posts-scraper").call(
run_input={"channels": ["telegram", "durov"], "maxPostsPerChannel": 200, "postedAfter": "7 days"}
)
for post in client.dataset(run["defaultDatasetId"]).iterate_items():
print(post["date"][:10], post["channel"], post["views"], (post["text"] or "")[:80])
3. Keep it running
In the Apify console: Schedules → daily → the same input with "postedAfter": "1 day". The Actor stops at the first post older than the cut-off, so a daily run over 20 channels is usually a few hundred posts — cents, or free on the free tier. Add an integration (Google Sheets, Slack, webhook) on the run's "Integrations" tab if you want the data pushed somewhere.
What it can't do
- Private channels, invite links (
t.me/+…), bots, groups without a web preview → the run reports "not a public channel" for that entry and continues with the rest. - Comments (discussion groups) aren't part of the preview.
- Counts are the rounded numbers Telegram shows (1.2K → 1200). Media URLs are CDN links that expire.
How it works under the hood
The preview page is plain HTML: 20 posts per page, ?before=<id> for older ones, reactions and views as text, media as CDN links.
If you'd rather build your own, the selectors are simple:
- each post is
div.tgme_widget_message[data-post="Channel/123"] - inside it:
time[datetime],.tgme_widget_message_text,.tgme_widget_message_views,.tgme_widget_message_reactions - the next page of history is the
a.tme_messages_more[data-before]link
Three gotchas I hit:
-
Usernames can be aliases.
t.me/s/proglibrenders the channelproglibrary— take the name fromdata-post, not from your input. -
Bots and channels with the preview disabled answer HTTP 200 with a normal-looking page but no
.tgme_channel_infoblock. Check for the block, not the status code. -
Post #1 is the "Channel created" service message (class
service_message) — skip those.
Disclosure
The Actor is mine. It's $0.50 per 1,000 posts after the free tier, and it's checked by an automated run every morning so it gets fixed when Telegram changes the markup: https://chorelet.app/p/telegram-channel-posts-scraper
Top comments (0)