How to Scrape Telegram: A Python Guide
- 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.
| Field | In web preview (t.me/s/) | Needs MTProto API | Notes |
|---|---|---|---|
| Channel title | Yes | - | .tgme_channel_info_header_title |
| Channel description | Yes | - | .tgme_channel_info_description |
| Subscriber count | Yes | - | Channel info counter |
| Photo / video / link totals | Yes | - | Channel info counters |
| Post id | Yes | - | data-post attribute (channel/425) |
| Post text | Yes | - | .tgme_widget_message_text |
| Exact timestamp | Yes | - | time[datetime], ISO 8601 |
| View count | Yes | - | .tgme_widget_message_views |
| Post media URL | Partial | Yes | Photos as background-image; some via API |
| Reactions / emoji counts | No | Yes | Not in preview markup |
| Comment / reply threads | No | Yes | Discussion groups via API |
| Member list | No | Yes | Never in web preview |
| Member usernames / phone | No | Yes | API only, access-gated |
| Full message history | Paginated | Yes | Preview 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.
| Limit | Where it hits | Severity |
|---|---|---|
| 20 posts per page | Web preview only | Low - just paginate with ?before= |
| No member list in preview | Web preview only | Hard - API-only by design |
| IP throttling at volume | Many requests, one IP | Medium - rotate IPs to scale |
| Private / preview-disabled channels | Closed channels | Hard - need MTProto + access rights |
| MTProto account bans | Member harvesting, spam | Severe - 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
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.
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/.
No, not for public channels with web preview enabled. A plain requests GET to t.me/s/
The whole public history, in 20-post pages. Each t.me/s/ request returns 20 messages; appending ?before=