Telegram Channel Scraper — Posts & Stats
Pricing
from $0.15 / 1,000 post returneds
Telegram Channel Scraper — Posts & Stats
Scrape any public Telegram channel as data. One row per post: text, date, views, photos, videos, links, link preview, author signature, edited flag, poll. Plus a channel row with subscribers, description and media counts. No login, no API key, no bot token.
Pricing
from $0.15 / 1,000 post returneds
Rating
0.0
(0)
Developer
Insight Solutions
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
13 hours ago
Last modified
Categories
Share
Any public Telegram channel, as a flat table. Give this Actor a channel name — durov, @telegram, https://t.me/s/durov, or a link to one of its posts — and get one row per post: the text, the publish date to the second, the view count, every photo and video URL, the links the author wrote, the link-preview card Telegram unfurled, the author signature and whether the post was edited. Each channel also gets a row of its own: subscriber count, description, verified flag, avatar, and Telegram's own counts of the photos, videos and links it has posted.
No login. No phone number. No bot token. No API key. $0.25 per 1,000 posts, proxy included, channels that could not be read are free, and a run that returns nothing costs nothing at all.
Try it in 30 seconds
{"channels": ["durov", "telegram"],"maxPostsPerChannel": 50,"includeChannelInfo": true}
Two channels, the fifty most recent posts of each. @durov, https://t.me/durov, https://t.me/s/durov and https://t.me/durov/527 all reach the same channel, and if you paste all four you are charged once.
Watching rather than harvesting? {"channels": [...], "since": "2026-08-01", "maxPostsPerChannel": 0} stops the moment it reaches a post older than the cutoff — one request per channel, however large the channel is. Want specific posts? {"postUrls": ["https://t.me/durov/300"]}.
What comes back
One post row per post:
{"ok": true,"rowType": "post", // "post" | "channel" | "diagnostic""channel": "durov", // the join key across every row of this channel"channelTitle": "Pavel Durov","postId": 527, // Telegram's own post number — dedupe on (channel, postId)"url": "https://t.me/durov/527","text": "🏆 Telegram is launching a $200,000 contest for content creators.\n\n🎬 Create a video about digital freedom…","textHtml": null, // the same post with its markup, when you ask for it"publishedAt": "2026-06-15T15:53:15.000Z","views": 24200000, // Telegram publishes "24.2M"; this is that, multiplied out"viewsText": "24.2M", // …and the original, so the rounding is visible"author": "Pavel Durov", // the signature, on channels that sign posts"edited": true,"mediaTypes": [], // photo | video | roundvideo | voice | sticker | document | poll | location | link"photos": null, // CDN URLs, one entry per photo in an album"videos": null, // {thumbnailUrl, videoUrl, duration, durationSec}"documents": null, // {title, size}"poll": null, // {question, type, options:[{text,percent}], votes}"linkPreview": null, // {url, siteName, title, description, imageUrl}"links": [ // every address linked from the text, in order"https://www.youtube.com/watch?v=1Yq_5aDdJ24","https://t.me/contest/456","https://t.me/contest"],"forwardedFrom": null, // {name, url} on a forwarded post"replyTo": null, // the t.me address of the post this one answers"position": 21, // 1-based within this channel, newest first"error": null,"errorType": null,"scrapedAt": "2026-09-11T16:44:02.101Z","source": "preview", // "preview" | "embed" | "info""sourceUrl": "https://t.me/durov/527"}
And one channel row per channel, written before its posts:
{"ok": true,"rowType": "channel","channel": "durov","channelTitle": "Pavel Durov","description": "Founder of Telegram.","subscribers": 10800000,"verified": true,"photoCount": 102, // Telegram's own counters for the channel…"videoCount": 46,"linkCount": 199,"fileCount": null, // …null where Telegram does not show that one"photoUrl": "https://cdn4.telesco.pe/file/ivioKGnD5ZXZWyYyw1LiL….jpg","url": "https://t.me/durov","source": "preview"}
Every row carries the same columns, so posts, channels and diagnostics export as one rectangular table. photoCount / videoCount / linkCount are the channel's counters; photos / videos / links are a post's lists. They are different columns because a column cannot be an integer on one row and an array on the next.
Use cases
- Channel monitoring. A scheduled run with
sinceset to your last run, keyed on(channel, postId), gives you every new post within the hour — and costs one request per channel, because the cutoff stops the walk rather than filtering after it. - Reach and engagement tracking.
viewson every post,subscriberson every channel,publishedAtto the second. Run it daily and you have a time series of how a channel's reach is moving, and how quickly a post accumulates views. - News and OSINT collection. Public channels are a primary source for breaking events, and this returns them as text with timestamps, media URLs and outgoing links — the shape an indexing or alerting pipeline wants.
- Competitive and market research. Posting cadence, media mix (
mediaTypes), which links a channel pushes, how often it edits, whether posts are signed. Compare across a list of channels in one run. - Crypto, trading and announcement feeds. A great deal of project communication happens only in Telegram.
linksandlinkPreviewgive you the outgoing addresses without parsing the text yourself. - Content and LLM pipelines. Post text plus date plus reach, as a flat array — a labelled corpus without the labelling.
- Archiving a channel.
maxPostsPerChannel: 0walks as deep as Telegram's public preview will serve, media URLs included.
How it works, and why it keeps working
Telegram publishes a web preview of every public channel at https://t.me/s/<channel>. It is the page Telegram itself serves to someone who clicks a channel link without the app installed — no account, no API, no rate-limited bot token — and it carries the whole post: text with formatting, the media, the view count, the exact timestamp.
| Surface | What it reads | Shape |
|---|---|---|
| Channel preview | t.me/s/<channel> | HTML. 20 posts per page, plus the channel header |
| Previous page | t.me/s/<channel>?before=<postId> | HTML. The 20 posts before that one |
| Post embed | t.me/<channel>/<id>?embed=1 | HTML. One post, in the same markup |
| Channel info | t.me/<channel> | HTML. Title, description, exact subscriber count — the fallback |
Four things catch naive scrapers here, and all four are handled:
There is no 404. A name that is private, does not exist, belongs to a bot, or is a channel with the web preview switched off all produce the same thing: HTTP 200, a redirect away from /s/, and a "Telegram: Contact @name" page. A client that checks status codes sees success and stores an empty result. This Actor detects the redirect and the page title, then reads the page it landed on to say which of the four it was — a channel with the preview off still yields its title and subscriber count, and gets a channel row.
Paging is a cursor, not a page number. The "load more" anchor carries data-before, and the page also carries a data-after anchor pointing the other way. Following the wrong one walks forwards for ever. And when a channel runs out, Telegram answers the cursor by serving the same page again rather than an empty one — so the walk tracks the post IDs it has delivered and stops when a page brings nothing new, instead of paying for the same twenty posts fifty times.
The markup is a template, and the classes collide. tgme_widget_message_photo is a prefix of tgme_widget_message_photo_wrap; the reply block contains an author name that is not the post's author; a video's duration sits outside the element you would expect. The parser matches class names on word boundaries, lifts the reply block out before reading anything else, and finds element ends with a depth counter rather than a regex.
Posts come oldest-first and you want newest-first. Every page is reversed as it is parsed, so the walk is one continuous newest-to-oldest stream. That is what makes since a stop rather than a filter, and it is the difference between a daily monitor costing one request per channel and costing fifty.
Underneath: Apify proxy, one pinned session per parallel worker, so one rate-limited channel burns only its own exit IP. When Telegram refuses an exit — HTTP 429, HTTP 403, an empty body, a page that is not a Telegram page — that session is retired and the same request goes out once more from a different address. Retrying on an address that was just refused only deepens the block, so it is never done. If the second address is refused too, the channel stops, keeps every row it already delivered, and files one free blocked row saying where it stopped. Pages of one channel are spaced 300–700 ms apart.
How it compares
- Nothing to sign up for. No phone number, no
api_id/api_hash, no bot added to the channel, no session string to keep alive. This reads the page Telegram serves to the public web, which is also why it runs under Apify's limited permissions. - Failures are free and legible. A private invite link, a channel that does not exist, one with the preview switched off, a name that cannot be a username, a block — each produces a diagnostic row with an
errorTypeyou can branch on, and no charge. A run that returns nothing at all finishes FAILED with the reason in its status message, never a green run containing an apology. sinceis a stop, not a filter. Most tools fetch everything and filter afterwards, which means a daily monitor pays the same as a full archive. Here a cutoff of yesterday costs one request per channel.- A partial run is kept, not thrown away. Hit
maxRunSecs, a block, or your charge ceiling halfway through a channel and you keep the posts already written. - Duplicates are collapsed before anything is fetched. Six spellings of one channel in your input list is one request and one bill.
- Honest about what is not here. No private channels, no groups, no comments, no reactions, no message history beyond what the public preview serves — see Limits below, in detail, rather than as a footnote.
Input reference
| Field | Type | Default | What it does |
|---|---|---|---|
channels | array of strings | ["durov","telegram"] | Usernames, @names, t.me/… links, t.me/s/… links, or links to a post. Duplicates across spellings are read, and billed, once |
maxPostsPerChannel | integer | 100 | Posts per channel, newest first. 20 per request, so 100 is 5 requests. 0 = as deep as the preview will go |
since | string | — | ISO date or timestamp. Stops the walk at the first older post, so a recent cutoff costs one request per channel |
includeChannelInfo | boolean | true | Write one channel row per channel — subscribers, description, verified, avatar, media counters |
postUrls | array of strings | [] | Links to individual posts, read through the embed widget. One row each, no channel row |
includeHtml | boolean | false | Add textHtml — the post exactly as Telegram marked it up |
maxPagesPerChannel | integer | 50 | Hard ceiling on requests per channel — the safety rail for maxPostsPerChannel: 0 |
maxConcurrency | integer | 4 | Channels read in parallel. Each worker keeps its own proxy session |
maxRunSecs | integer | 240 | Whole-run wall-clock budget. When it runs out the Actor keeps what it has and files a free diagnostic row for each channel it never reached |
proxyConfiguration | object | Apify datacenter | Telegram answers datacenter addresses, so the cheap proxy is the default |
Output reference
| Column | On which rows | What it is |
|---|---|---|
ok, rowType | all | true plus post / channel, or false plus diagnostic |
input | all | The entry this row came from, as you supplied it |
channel | all | The channel's @username without the @ — the join key |
channelTitle | all | The channel's display name. Null on rows from postUrls |
postId, url | post | Telegram's post number, and the public address. Gaps in the numbers are deleted posts |
text, textHtml | post | The post as plain text (clipped to 8,000 chars), and with markup when asked |
publishedAt | post | ISO 8601 UTC, to the second |
views, viewsText | post | The view count as an integer, and exactly as Telegram printed it |
author, edited | post | The signature where a channel signs posts; whether Telegram marks the post edited |
mediaTypes | post | photo, video, roundvideo, voice, sticker, document, poll, location, link |
photos, videos, documents | post | Media URLs and metadata. An album is several photos on one row |
poll, linkPreview | post | The poll with its options and percentages; Telegram's own unfurl of the first link |
links | post | Every address linked from the text, in order, deduplicated |
forwardedFrom, replyTo | post | Where a forward came from; the post a reply answers |
position | post | 1-based within the channel, newest first |
description, subscribers, verified, photoUrl | channel | The About text, follower count, verification tick and avatar |
photoCount, videoCount, linkCount, fileCount | channel | Telegram's own counters. Null where Telegram does not show one |
source, sourceUrl | all | Which surface the row came from, and the exact address |
error, errorType | diagnostic | What went wrong, and a type to branch on |
scrapedAt | all | When the row was written |
errorType is one of not-found, no-results, invalid-input, blocked, timeout, unavailable.
Pricing
$0.25 per 1,000 posts. Pay-per-event, with proxy already inside that number — there is no separate proxy line on your bill for this Actor.
| Event | What triggers it | FREE | Starter | Scale | Business |
|---|---|---|---|---|---|
| Post returned (primary) | One post row written to your dataset | $0.00025 | $0.00025 | $0.0002 | $0.00015 |
| Channel record returned | One channel row — subscribers, description, counters | $0.001 | $0.001 | $0.001 | $0.001 |
| Run started | Once per run, after the first paid row | $0.001 | $0.001 | $0.001 | $0.001 |
Worked example. 20 channels, 100 newest posts each, one of them private and one name misspelled:
- 18 channels × 100 posts = 1,800 posts × $0.00025 = $0.45
- 18 channel rows × $0.001 = $0.018
- 1 run start = $0.001
- 1 private channel, 1 unusable name = $0.00 (free diagnostic rows)
- Total: $0.469
Daily monitor example. 50 channels, since set to yesterday, about 5 new posts each: 250 posts × $0.00025 + 50 channel rows × $0.001 + $0.001 = $0.1135 per run, about $3.41 a month. Turn includeChannelInfo off and it is $0.0635 per run, about $1.91 a month — the channel row is the part that scales with how many channels you follow, so it is the one to switch off when you are following a lot of them.
Archive example. One channel, maxPostsPerChannel: 0, 1,000 posts: 1,000 × $0.00025 + $0.001 + $0.001 = $0.252, from 50 requests.
What you are never charged for: a channel that does not exist, a private one, one whose web preview is switched off, a private invite link, an entry that was not a Telegram username, a channel with no posts, a channel the run never reached before maxRunSecs, or a request Telegram blocked. If a whole run comes back empty it finishes FAILED and bills nothing at all, start fee included.
Set ACTOR_MAX_TOTAL_CHARGE_USD on a run and the Actor stops fetching once the ceiling is in sight, rather than handing you rows it cannot bill or billing you for rows it cannot hand over. It finishes SUCCEEDED with the ceiling named in its status message, and everything already delivered is yours.
Limits, and the ones that might bite
Public channels only. Private channels, groups, supergroups, direct messages and anything behind an invite link are not readable without an account, and this Actor never has one. A t.me/+… invite link or a t.me/c/… internal link gets a free diagnostic row saying exactly that, rather than a misleading "not found".
Some public channels have the web preview switched off. It is a per-channel setting, and a channel that has it off is unreadable here even though it is public in the app — bbcnews is a real example. You still get a channel row with its title and subscriber count where Telegram publishes them, plus a free not-found row explaining why there are no posts.
How deep you can go is Telegram's choice, not a setting. The preview serves 20 posts a page and keeps handing out cursors for a while, but it stops well short of a large channel's first post — and when it stops it repeats the last page rather than saying so. This Actor notices the repeat and ends the walk. There is no way to reach a channel's entire history from the public web.
View counts are rounded at the source. Telegram publishes 24.2M, not 24,183,551. views is that abbreviation multiplied out and viewsText keeps the original, so the precision you are getting is visible rather than implied. Subscriber counts from a channel header are rounded the same way; the ones read from the info page are exact.
No reactions, no comments, no forward counts. Telegram renders reaction counts in the preview but they are not exposed as data here, and the discussion-group comments under a post live in a different chat that needs an account. Forward counts are not published anywhere public.
Media URLs are signed and time-limited. photos, videos and photoUrl point at Telegram's CDN with a token in the address. They work for a while after the run and then stop. Fetch what you need promptly; this Actor never downloads media itself.
Some post shapes are implemented but unverified against live markup. None of the pages captured while building this carried a forwarded post, a reply, an attached document, a poll, a sticker, a voice note or a round video. Those parsers are written from Telegram's own class names and unit-tested against synthetic markup, so forwardedFrom, replyTo, documents and poll may be null on a channel that uses them until the first live run confirms the shape. Everything the captures did contain — text, dates, views, photos, albums, videos, link previews, signatures, edited flags, the channel header, the paging cursor and the not-found redirect — is verified against real bytes. TEST_RESULTS.md lists exactly which is which.
Post order is Telegram's order. Rows come back newest first, and position reflects that. A gap in postId is a deleted post, not a missing row.
The upstream format may change. Telegram changes its web preview markup without notice — that is true of every tool that reads this data, including the ones that do not say so. When a shape changes, rows stop arriving and you get free blocked or no-results diagnostic rows rather than quietly wrong data, and a run that returns nothing bills nothing.
Rate and reliability. Requests go out through proxy sessions pinned per worker, one rotation per block, and a 300–700 ms pause between pages of the same channel. Four channels in parallel is the default. Telegram rate-limits by address: if you run hundreds of channels on a tight schedule and start seeing blocked rows, lower maxConcurrency or switch proxyConfiguration to residential.
Use it from an AI agent, or from code
One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with limited permissions, uses pay-per-event pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The Integrations tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.
curl -X POST "https://api.apify.com/v2/acts/insight.solutions~telegram-channel-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"channels":["durov"],"maxPostsPerChannel":50}'
# pip install apify-clientfrom apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")run = client.actor("insight.solutions/telegram-channel-scraper").call(run_input={"channels": ["durov", "@telegram"],"since": "2026-08-01","maxPostsPerChannel": 0,"includeChannelInfo": False,})for row in client.dataset(run["defaultDatasetId"]).iterate_items():if row["rowType"] == "post":print(row["publishedAt"], row["views"], row["text"][:80])
FAQ
Do I need a Telegram account, a phone number or a bot token?
No. Everything here comes from the public web preview Telegram serves at t.me/s/<channel> — the page anyone gets by clicking a channel link in a browser.
How do I deduplicate between runs?
On (channel, postId). postId is Telegram's own number for the post and never changes. An incremental pipeline is an upsert on that pair, with since set to the last run's timestamp.
Can I get every post a channel has ever made?
Only as far as Telegram's preview will serve, which for a large channel is a few hundred to a few thousand posts, not the whole history. maxPostsPerChannel: 0 goes as deep as it can and stops when Telegram stops.
Why did I get fewer posts than I asked for?
The channel has fewer, your since cutoff was reached, maxPagesPerChannel was reached, or Telegram stopped serving older pages. The run's status message says which.
Why is views a round number?
Because Telegram publishes it as one — 24.2M. viewsText holds the original string so you can see exactly how much precision there is.
A channel works in my Telegram app but this says not-found. Why?
Its web preview is switched off, or it is private. Open https://t.me/s/<channel> in a browser: if it redirects to the plain channel page, there is no public preview to read. The diagnostic row says which case it is.
Can I read a channel's comments or reactions? No. Comments live in a linked discussion group that needs an account, and reaction counts are not exposed as data here.
What happens if one channel fails?
The others still run. The failed one produces a free diagnostic row and the run finishes SUCCEEDED. If every entry fails, the run finishes FAILED and you are billed nothing at all.
Is the data fresh? Live. Every run reads Telegram at that moment; nothing is cached.
Legal and data-protection notes
- Public pages only.
t.me/s/<channel>is the page Telegram publishes for the open web, and it is what a search engine indexes. The Actor never logs in, never accepts a session token or a bot token, and never reads a private channel, a group or a direct message. - Channel posts belong to their authors. Post text, photos and video are copyrighted works. Indexing, analysis, monitoring and linking are the ordinary uses and are what this is built for; republishing is your call and your responsibility.
- Some public channels are run by identifiable people. A post, an author signature and a channel description can be personal data even when they are public. If you process them for anything beyond your own analysis, have a lawful basis, and check the rules where you and the author are.
- Media URLs are not a licence. They are the addresses a browser would fetch to render the preview. Downloading them at scale is a cost to Telegram's CDN; this Actor never downloads media.
- Not affiliated with Telegram FZ-LLC or with any channel whose content you retrieve. All names and trademarks belong to their owners and are used only to describe which public pages this Actor reads.
Our other Actors
Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.
Video, audio & social
- YouTube Transcript API — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- YouTube Comments API — comments and replies with likes, pinned and hearted flags, newest or top sort.
- YouTube Channel API — a channel's videos, Shorts and live streams, plus YouTube search.
- Podcast Search, Episodes & Charts API — Apple Podcasts search, charts and full episode feeds.
- Bluesky Scraper — profiles, posts, followers and follows from the public AT Protocol API.
- Substack Scraper — posts with full free text, comments and publication profiles.
News, documents & the web
- Google News Search, Topics & Real Article URLs — news search and topic feeds with the publisher's real URL decoded.
- Website to Markdown — Content Extractor for LLMs & RAG — any site as clean Markdown, text and heading-aware chunks.
- Internet Archive API — archive.org search, item metadata, files and reviews.
- Wayback Machine Toolkit — archived URL inventories, snapshots and text diffs between dates.
- Website Technology Detector — the tech stack behind any site, with the evidence for each detection.
- Domain Intelligence API — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- SEO Page Audit — sitemap crawl with on-page checks, structured data and broken-link reports.
- Keyword Suggestions API — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- Website Contact Extractor — emails, phone numbers and social profiles from any list of websites.
Business, finance & jobs
- Congress & Insider Trades API — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- SEC EDGAR API — filings, XBRL financials and full-text search by ticker or CIK.
- Y Combinator Companies, Batches & Founders — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- Career Site Jobs API — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- New Job Postings Monitor — new, closed and changed postings on the career sites you watch.
- Shopify Products API — any Shopify store's catalogue, variants, prices and stock signals.
Apps & games
- App Store & Google Play Reviews API — reviews from both stores with ratings, versions and developer replies.
- App Store Top Charts & App Search API — Apple top charts by country and genre, plus app search and details.
- Steam Reviews API — Steam reviews with playtime, helpfulness and game details.
- Steam Game Data API — prices, tags, review scores, live player counts and top charts.