~ / guides / How to Scrape Telegram: A Python Guide

How to Scrape Telegram: A Python Guide

MR
Marcus Reed
Founder & lead tester · about the author
the short version
  • Public Telegram channels expose a server-rendered web preview at t.me/s/<channel>. I fetched it live in June 2026: HTTP 200, ~138 KB of HTML, and 20 fully parsed posts per page with no JavaScript needed. Real output is pasted below.
  • From the static HTML you get post id, exact ISO timestamp, view count, post text, and channel-level counters (subscribers, photos, videos, links). The web preview does not expose member lists or phone numbers.
  • Older posts page through a ?before=<post_id> cursor. I verified one hop back returned the previous 20 posts. There is no rate-limit header, but datacenter IPs get throttled at volume.
  • Member lists, full message history, and media downloads come from the official MTProto API via a library like Telethon, which needs your own phone number and an api_id. The web preview cannot reach those fields.
  • Telegram's Terms ban bulk data collection that harms users, and most channel content is personal data, so GDPR applies once you store info on identifiable EU users. For history at scale, a scraper API like ChocoData handles the IP rotation.

Telegram is a search engine blind spot full of public data: news channels, crypto signal groups, brand communities, and announcement feeds, most of it never indexed by Google. The catch is that Telegram is an app first, so people assume its data is locked inside the client. It is not for public channels. Telegram publishes a server-rendered web preview at t.me/s/<channel> that returns clean HTML with no JavaScript. I spent June 2026 testing exactly what that preview gives up, and it is more than I expected: post text, exact timestamps, and view counts, all parseable with requests and BeautifulSoup. This guide shows the real output I captured, the ?before= pagination trick, and where you have to drop to the official API or a scraper API instead.

Can you legally scrape Telegram?

Scraping public Telegram channels is not automatically illegal, but two layers of risk sit on top of the technical question, and member scraping is far riskier than post scraping. Separate them before you write code.

The first layer is Telegram’s own rules. The Telegram Terms of Service and the broader Terms for the API prohibit using the platform to send spam, and Telegram bans accounts that scrape user data for bulk messaging or harvesting. Public channel posts published for broadcast are a softer target than member rosters: pulling the text of a public announcement channel is low-risk, while extracting a group’s member list to message those people is the exact behavior Telegram’s anti-spam systems exist to stop. The dedicated “member adder” tools you find advertised operate squarely in that high-risk zone.

The second layer is data protection, and it is the one that bites. A Telegram post has an author, often opinions, sometimes names and locations, so it is personal data. The moment you store information about an identifiable EU resident, GDPR applies, and California’s CCPA/CPRA reaches residents there. I cover the general framing in my is web scraping legal guide. For Telegram specifically, the defensible posture is narrow: collect public channel posts for aggregate analysis (sentiment, trends, monitoring), avoid building profiles of named individuals, never use a scraped list to send unsolicited messages, and get counsel before storing anything tied to identifiable people. Public does not mean unrestricted.

What data is available on a public Telegram channel?

A public channel’s web preview exposes a defined set of fields, and a separate, larger set sits behind the official MTProto API. The table below splits them, because that gap decides which method you need. “Web preview” means what a plain requests call to t.me/s/<channel> returns; “MTProto API” means what the official protocol exposes through a library like Telethon.

FieldIn web preview (t.me/s/)Needs MTProto APINotes
Channel titleYes-.tgme_channel_info_header_title
Channel descriptionYes-.tgme_channel_info_description
Subscriber countYes-Channel info counter
Photo / video / link totalsYes-Channel info counters
Post idYes-data-post attribute (channel/425)
Post textYes-.tgme_widget_message_text
Exact timestampYes-time[datetime], ISO 8601
View countYes-.tgme_widget_message_views
Post media URLPartialYesPhotos as background-image; some via API
Reactions / emoji countsNoYesNot in preview markup
Comment / reply threadsNoYesDiscussion groups via API
Member listNoYesNever in web preview
Member usernames / phoneNoYesAPI only, access-gated
Full message historyPaginatedYesPreview pages 20 at a time

The practical line: post content, timing, and reach are scrapable from the HTML; anything about who is in the channel is API-only and access-gated. If your use case is “what is this channel publishing and how far does it reach,” the web preview gets you there. If it is “who are the members,” you need the MTProto API and the legal exposure that comes with it.

Can you scrape Telegram with Python requests? (Method 1: DIY)

Yes. I tested it on June 12, 2026, and a plain requests GET to t.me/s/<channel> returns HTTP 200 with the last 20 posts fully server-rendered, no JavaScript required. This is the result that tells you the DIY path is viable for public channels. Here is the exact code I ran:

import requests
from bs4 import BeautifulSoup

CHANNEL = "telegram"  # public channel username (no @)
URL = f"https://t.me/s/{CHANNEL}"
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                         "AppleWebKit/537.36 (KHTML, like Gecko) "
                         "Chrome/124.0 Safari/537.36"}

resp = requests.get(URL, headers=HEADERS, timeout=30)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")

posts = []
for msg in soup.select(".tgme_widget_message"):
    text_el = msg.select_one(".tgme_widget_message_text")
    time_el = msg.select_one(".tgme_widget_message_date time")
    views_el = msg.select_one(".tgme_widget_message_views")
    posts.append({
        "id": msg.get("data-post"),
        "datetime": time_el.get("datetime") if time_el else None,
        "views": views_el.get_text(strip=True) if views_el else None,
        "text": text_el.get_text(" ", strip=True) if text_el else "",
    })

print(f"Scraped {len(posts)} posts from @{CHANNEL}")
for p in posts[:3]:
    print(p["id"], "|", p["datetime"], "| views:", p["views"])
    snippet = (p["text"][:70] + "...") if len(p["text"]) > 70 else p["text"]
    print("   ", snippet)

Running it against the official @telegram channel returned this, unedited:

Scraped 20 posts from @telegram
telegram/425 | 2026-02-10T17:43:45+00:00 | views: 1.72M
    New Design. Telegram for Android has a fully redesigned interface that...
telegram/426 | 2026-02-10T17:44:05+00:00 | views: 1.74M
    Gift Crafting. Collectible gifts can now be combined to create exclusi...
telegram/427 | 2026-02-10T17:44:22+00:00 | views: 2.3M
    Colored Buttons for Bots. Bot developers can now assign colors and emo...

Real post ids, ISO 8601 timestamps, view counts in Telegram’s 1.72M format, and post text. No login, no key. If you want the channel-level numbers too, the same page carries them. I also pulled the counters in my test and got 10.4M subscribers, 13 photos, 219 videos, and 363 links from these selectors:

title = soup.select_one(".tgme_channel_info_header_title")
for c in soup.select(".tgme_channel_info_counter"):
    value = c.select_one(".counter_value")
    label = c.select_one(".counter_type")
    print(value.get_text(strip=True), label.get_text(strip=True))

The whole approach rests on BeautifulSoup selectors, the same pattern I use in my BeautifulSoup guide and the broader Python scraping walkthrough.

How do you scrape a channel’s full history?

You page backwards with a ?before=<post_id> cursor, 20 posts at a time. The web preview only shows the latest 20 messages, but appending ?before= and the oldest id you have seen returns the previous 20. I verified this: a base request to @telegram returned posts 425 through 445, and t.me/s/telegram?before=425 returned posts 405 through 424 with HTTP 200. Loop that down to post 1 and you walk the entire public archive.

import requests, time
from bs4 import BeautifulSoup

CHANNEL = "telegram"
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                         "AppleWebKit/537.36 (KHTML, like Gecko) "
                         "Chrome/124.0 Safari/537.36"}

def fetch_page(before=None):
    url = f"https://t.me/s/{CHANNEL}"
    params = {"before": before} if before else {}
    r = requests.get(url, headers=HEADERS, params=params, timeout=30)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    return [m.get("data-post") for m in soup.select(".tgme_widget_message")]

all_ids, cursor = [], None
for _ in range(5):  # 5 pages = up to 100 posts; raise the cap to go deeper
    ids = fetch_page(cursor)
    if not ids:
        break
    all_ids.extend(ids)
    cursor = ids[0].split("/")[1]  # oldest id on this page -> next ?before
    time.sleep(1)  # be polite; datacenter IPs get throttled without it

print(f"Collected {len(all_ids)} post ids")

The time.sleep(1) matters. Telegram publishes no robots.txt (I checked: t.me/robots.txt returns 404) and sends no rate-limit header, so there is no documented quota. The real limit is IP-based throttling. Pull thousands of pages from one datacenter address and requests start hanging or returning empty bodies. For one channel this is fine; for hundreds of channels in parallel it is the wall that pushes you to rotating IPs.

How does Telegram block scrapers?

Telegram does not fight the web preview much, but four limits shape what you can collect, and the hardest ones guard member data. Here is what I observed and what is documented.

LimitWhere it hitsSeverity
20 posts per pageWeb preview onlyLow - just paginate with ?before=
No member list in previewWeb preview onlyHard - API-only by design
IP throttling at volumeMany requests, one IPMedium - rotate IPs to scale
Private / preview-disabled channelsClosed channelsHard - need MTProto + access rights
MTProto account bansMember harvesting, spamSevere - phone-number ban

The web preview itself is generous: no login wall, no JavaScript, no token, HTTP 200 from a logged-out request. The blocking happens at two edges. First, anything beyond public posts (members, reactions, private channels) is simply absent from the preview and forces you onto the MTProto API. Second, the MTProto API is where Telegram polices hard: accounts that harvest members or send bulk messages get phone-number-level bans, because that account is tied to a real SIM. That asymmetry is the whole strategy: harmless public-broadcast data is easy, user-harvesting data is gated and policed.

Method 2: Use a scraper API

When you need many channels at once, full histories, or you keep hitting IP throttling, a scraper API takes over the rotation and retry work so you stay on parsing. The DIY path above is the right tool for a handful of public channels. At volume, the IP problem becomes the whole job, and that is what an API absorbs.

ChocoData is one such service, with a universal endpoint plus a set of dedicated endpoints. Its request shape is a single GET carrying your target URL, your API key, and an optional flag to route through a residential IP. Here is the pattern for sending a t.me/s/ URL through the universal endpoint:

import requests

API_KEY = "your_chocodata_key"

resp = requests.get(
    "https://api.chocodata.com/api/v1/universal",
    params={
        "api_key": API_KEY,
        "url": "https://t.me/s/telegram",
        "country": "us",   # route via a residential IP to dodge throttling
    },
    timeout=90,
)

html = resp.text
# The t.me/s/ page is already static HTML, so parse the response
# with the same BeautifulSoup selectors from Method 1. The API's
# job here is the IP rotation, not JavaScript rendering.

I did not run this snippet, because it needs a paid key, so treat the field names as illustrative and confirm the exact parameters against ChocoData’s own docs before you build on them. The point is the division of labor: the t.me/s/ HTML is already server-rendered, so you reuse the Method 1 parser unchanged and let the API handle the IP rotation that breaks a single-address scraper at scale. For the general trade-offs of when an API beats DIY, see my scraping without getting blocked guide.

For member lists, reactions, and private channels the web preview cannot touch, neither the DIY scraper nor a URL-based API helps; that data lives behind the official MTProto API. The library most people use is Telethon, authenticated with an api_id and api_hash from my.telegram.org plus your own phone number. That path carries the account-ban and GDPR exposure described above, so reserve it for cases where public posts genuinely are not enough.

Which method should you use?

Match the method to the data, and the choice is clear. For public channel posts, timestamps, and view counts, the DIY t.me/s/ scraper in Method 1 is all you need, and it is what I tested and verified above. For the same data across many channels or full archives where IP throttling bites, add a scraper API to handle rotation. For member lists, reactions, or private channels, only the official MTProto API reaches those fields, and it brings real account and legal risk. Start with the web preview, escalate only when the data you need is genuinely absent from it.

FAQ

Can I get a Telegram group's member list by scraping t.me?

No. The t.me web preview only renders posts, channel title, description, and aggregate counters (subscriber count, photo/video/link totals). It never lists individual members or their usernames. To pull a member roster you need the official MTProto API through a library like Telethon, authenticated with your own phone number and api_id from my.telegram.org, and even then only for groups you can access. Member scraping is also the activity Telegram polices most aggressively, so treat it as the high-risk path.

What is the difference between t.me/channel and t.me/s/channel?

The /s/ prefix is the key. t.me/telegram returns an 11 KB landing page with an 'open in app' button and almost no content. t.me/s/telegram returns a 138 KB page with the last 20 posts fully server-rendered in HTML. In my June 2026 test only the /s/ version contained parseable post data, so always build your scraper against t.me/s/.

Does scraping public Telegram channels need an API key?

No, not for public channels with web preview enabled. A plain requests GET to t.me/s/ returned HTTP 200 and 20 posts for me with no key, no login, and no token. You only need credentials for the MTProto API, which covers private groups, member lists, and full archives the web preview omits. The web-preview path is the simplest and is what this guide tests.

How many posts can I scrape from one Telegram channel?

The whole public history, in 20-post pages. Each t.me/s/ request returns 20 messages; appending ?before= returns the previous 20. Loop that cursor down to post 1 and you walk the entire archive. The wall is not a post cap, it is IP throttling: hammer t.me from one datacenter address and requests start failing, which is where rotating IPs or a scraper API come in.

MR
Marcus Reed
I've built and run web scrapers for the better part of a decade. On this site I put scraper APIs and scraping tools through real jobs against real targets, then write up what actually holds up.