Telegram's official APIs are great, but both come with strings attached: the Bot API needs a bot that channel owners must add, and the MTProto client API wants your phone number and an app registration. If all you need is the public message history of public channels, there is a third door with no strings at all.
The t.me preview pages
Every public channel has a web preview:
GET https://t.me/s/durov
That returns the latest ~20 messages as server-rendered HTML. No key, no cookie, no login. And it paginates:
GET https://t.me/s/durov?before=510
gives you the 20 messages older than message id 510. Walk before= down and you can read a channel's history as far back as you care to go.
What is in the HTML
Each message sits in a .tgme_widget_message block with a data-post="channel/id" attribute. Inside:
-
.tgme_widget_message_textholds the message body, links included -
time[datetime]gives a clean ISO 8601 timestamp -
.tgme_widget_message_viewsshows the view count, abbreviated (3.4M) -
.tgme_widget_message_photo_wraphides photo URLs in its inlinebackground-imagestyle -
.tgme_widget_message_forwarded_from_namenames the source channel of a forward, with link
The channel card at t.me/<channel> (without /s/) adds the title, bio and subscriber count via ordinary OpenGraph meta tags plus a .tgme_page_extra element.
Two parsing gotchas
Abbreviated and spaced numbers. View counts come as 3.4M or 12.3K, and subscriber counts as 11 862 837 with narrow no-break spaces (U+202F) between groups, which will break a naive parseInt. Normalize both:
function parseCount(raw) {
const s = raw.replace(/[\s ]/g, '').toLowerCase();
const m = s.match(/^([\d.]+)([km]?)$/);
if (!m) return null;
return Math.round(parseFloat(m[1]) * (m[2] === 'm' ? 1e6 : m[2] === 'k' ? 1e3 : 1));
}
Not every channel has a preview. Private channels, most groups, and channels that disabled previews return a page with no message blocks. Detect the empty case and report it honestly instead of retrying.
What the preview does not have
Reactions, comment counts and poll results are not in the public preview, so no keyless scraper can offer them truthfully. If a tool claims them without asking for API credentials, be suspicious.
Be polite and it just works
The preview pages are served to every browser and link previewer on the internet, so they are tolerant, even from datacenter IPs. Pace your requests a few hundred milliseconds apart and paginate only as deep as you need.
If you want it as a service
I packaged this as a pay per use actor: public channel usernames in, one JSON row per message out (text, date, parsed views, links, media, forward source), plus a free channel info row with the subscriber count: https://apify.com/scrapemint/telegram-channel-scraper
Same theme as the rest of my actors: find the public endpoint a platform already serves, skip the browser, and the data costs almost nothing.
Top comments (0)