Telegram Channel Scraper — Posts, Reach & Keyword Search
No login. No phone number. No bot token.
Reads the public preview pages Telegram serves to any anonymous visitor, so there is no account to create, no phone number to burn and no session to expire. Full post text, view counts, reactions, links, inline buttons and exact subscriber counts, plus keyword search that runs on Telegram's side rather than on yours.
oswaldocarabano/telegram-channel-scraper
{
"mode": "search",
"channels": ["durov", "https://t.me/s/tgbeta"],
"searchTerms": ["react", "typescript"],
"exactMatch": true,
"maxMessagesPerChannel": 200
}- Version
- v0.1.12
- Memory
- 512 MB
- Browser
- none
- Proxy
- None — direct, with an optional fallback
Short answer
The Telegram Channel Scraper Actor extracts posts from public Telegram channels without a login, a phone number, a bot token or an API key, by reading the same t.me preview pages Telegram serves to anonymous visitors. It returns post text at a 99.6% fill rate measured over 893 posts across 9 channels, view counts at 99.2%, links at 80.7%, and exact subscriber counts rather than the rounded 11.1M the preview displays. Keyword search runs on Telegram's side: finding 20 posts mentioning a term took 1 request and 24 KB against 25 requests and 712 KB for reading the channel. Pricing is $0.001 per post or search result and $0.002 per channel record, and error rows are never charged.
Key points
- No login, no phone number, no bot token and no API key: it reads the public preview pages Telegram serves to any anonymous visitor, so there is no session to expire and no account to ban.
- Keyword search runs on Telegram's side. The same 20 matching posts cost 1 request and 24 KB through search, against 25 requests and 712 KB by reading the channel's history.
- Exact subscriber counts. The preview shows a rounded `11.1M`; one extra 4 KB request returns `11143438`, and both numbers are delivered so the conversion can be audited.
- Fill rates measured over 893 posts across 9 channels: text 99.6%, views 99.2%, links 80.7%, t.me handles 43.2%, reactions 11.0%, emails 3.0% overall and 9.0% in job channels.
- Telegram's search stems rather than matches substrings, so `hiring` returns posts saying `hire a cab`. Those false positives are removed before delivery and are never charged.
- A channel that cannot be read gets a free diagnostic row saying why — it does not exist, or it is a group, a bot, a user account, or a channel with its web preview switched off.
What it does
Reads the public preview pages Telegram serves to any anonymous visitor, so there is no account to create, no phone number to burn and no session to expire. Full post text, view counts, reactions, links, inline buttons and exact subscriber counts, plus keyword search that runs on Telegram's side rather than on yours.
{
"channel_username": "durov",
"telegram_channel_id": "1006503122",
"message_id": "421",
"url": "https://t.me/durov/421",
"datetime": "2026-08-14T09:31:04+00:00",
"text": "Telegram now supports…",
"views": 3140000,
"views_raw": "3.1M",
"reactions_total": 48213,
"links": ["https://telegram.org/blog/"],
"link_domains": ["telegram.org"],
"mentions": [],
"tme_handles": [],
"hashtags": [],
"emails": [],
"has_photo": true,
"has_video": false,
"forwarded_from": null,
"is_edited": false,
"from_cache": false,
"data_age_hours": 0
}Why this one
Anonymous by design, not by limitation
Scrapers that sign in with a phone number can reach more — private channels, attachments, member lists — and they get the account banned, at which point the pipeline stops. This Actor reads what Telegram publishes to the open web. There is nothing to authenticate, so there is nothing that expires halfway through a run, and the same input works the same way in six months.
Search that costs a request, not a crawl
Reading a whole channel's history to find the posts mentioning one term is the expensive way to do it: measured at 25 requests and 712 KB for 20 results in a job channel. Telegram's own search endpoint returns the same 20 in 1 request and 24 KB. You pay per relevant row rather than per page walked past, which is why search mode and messages mode carry the same price per row.
Both numbers, never just the rounded one
Every abbreviated figure Telegram displays is delivered as a pair. `views` is the typed value and `views_raw` is the string as shown; `subscribers` is exact where the preview said `11.1M`. A rounded number passed off as a measurement is the failure mode of every reach report built on this data, and delivering both is what makes the rounding auditable rather than invisible.
Diagnostics instead of an empty row
When a handle does not resolve, most tools return nothing and leave you guessing. This one spends an extra 4 KB request to say which of five things happened: no such channel, a group, a bot, a user account, or a public channel whose web preview is switched off. Those rows are free, because a diagnostic is not a delivery.
The identifier that survives a rename
`telegram_channel_id` is Telegram's internal id and it is on every row. A channel that changes its `@username` breaks any dataset keyed on the handle; keyed on the internal id, the history stays joined.
Use cases
- Track what a set of public channels published, with view counts and reactions as the reach signal.
- Find posts mentioning a technology, a company or a product across many channels, without knowing the channel names, using discover mode over the curated catalogue.
- Build a reach report with exact subscriber counts rather than the rounded figures the preview displays.
- Monitor job channels for postings mentioning a stack, and read the contact handles the post carries.
- Measure how a story propagates, using `forwarded_from` and the post datetime.
Input
Every field has a default, and the defaults are deliberately small so a first run is cheap enough to inspect before you commit to a sweep. This table mirrors the Actor's own input schema field for field.
| Field | Default | What it does |
|---|---|---|
modestring | "messages" | What to do`messages` reads a channel's posts, `search` returns only posts matching your keywords with the filtering done on Telegram's side, `discover` searches the curated catalogue by niche so you need no channel names, `channel-info` returns metadata and the exact subscriber count only. |
channelsstring[] | [] | ChannelsHandles or t.me URLs. `durov`, `@durov`, `t.me/durov` and `https://t.me/s/durov` all mean the same channel. Not needed in discover mode. |
searchTermsstring[] | [] | Search termsRequired in search and discover modes. Telegram filters on its side, which is far cheaper than reading a whole channel. |
exactMatchboolean | true | Exact match onlyRemoves the stemming false positives Telegram's search returns — a query for `hiring` matching `hire a cab`. Costs nothing, and a discarded candidate is never charged. |
nichestring | "all" | Niche (discover mode)Which part of the curated catalogue to search: jobs, business, crypto, tech, news, marketing, education, health, law, lifestyle, culture, sport, social, other, or all. |
maxChannelsinteger | 40 | Maximum channels (discover mode)How many catalogue channels to search, largest first. Each one costs a request. |
maxMessagesPerChannelinteger | 200 | Maximum posts per channelA hard cap, so a large channel cannot run up an unexpected bill. Discover mode defaults to 20 instead, because it opens many channels at once. |
newestFirstboolean | true | Newest posts firstTurning it off walks the channel forward from the oldest post, which is what makes a long backfill resumable across runs. |
sinceDatestring | "" | Only posts afterISO date. Walking newest-first stops as soon as it passes this date, so you are not charged for posts you did not want. |
untilDatestring | "" | Only posts beforeISO date. |
includeExactSubscribersboolean | true | Exact subscriber countOne extra 4 KB page turns the preview's rounded `11.1M` into `11143438`. Ignored in discover mode. |
includeContactFieldsboolean | true | Include contact entitiespersonal dataEmails, phone numbers, @mentions and t.me handles found in the post text, each in its own column. Turn it off and those columns come back empty; the post text is returned in full either way. |
maxCacheAgeDaysnumber | 1 | Accept cached pages up to (days)Every row declares `from_cache` and `data_age_hours`, so a cached row never passes as a fresh one. 0 forces a fresh fetch. |
maxConcurrencyinteger | 5 | Parallel requestsCapped at 5 deliberately. These are public pages served to anonymous visitors and the Actor stays well inside polite crawling limits. |
proxyFallbackboolean | true | Proxy fallbackRequests go direct, which costs nothing and is what works — no rate limiting has been observed on these pages. This retries through a sticky proxy session if Telegram starts refusing, rather than silently returning fewer rows. |
Output and fill rates
A field being in the schema is not the same as it having a value. The percentages below were counted on real runs; the sample sizes are in Measurements. Anything not listed here is not promised.
| Field | Filled | Meaning |
|---|---|---|
datetimedatetime | 100% | ISO 8601, exact to the second. |
telegram_channel_idstring | 100% | Telegram's internal channel id. Survives a channel renaming its `@username`, which the handle does not. |
message_idstring | 100% | With `url` and `channel_username`. |
textstring | 99.6% | The full post text, never truncated. |
viewsinteger | 99.2% | With `views_raw`, the string as Telegram displayed it, so the rounding is auditable. |
linksstring[] | 80.7% | With `link_domains`. Read from the link target rather than from the visible text. |
has_photoboolean | 52.4% | Whether the post carries a photo. The file itself is not extracted. |
tme_handlesstring[] | 43.2% | `https://t.me/...` contacts in the text. More common than @mentions, and given its own column. 64.2% in job channels. |
hashtagsstring[] | 37.4% | Hashtags in the post text. |
mentionsstring[] | 30.8% | `@handle` mentions. 53.5% in job channels. |
has_videoboolean | 11.6% | Whether the post carries a video. |
reactionsobject | 11% | With `reactions_total`. Only channels with reactions enabled. |
authorstring | 11% | Only on channels that sign their posts. |
is_replyboolean | 11% | With `reply_to_url`. |
forwarded_fromstring | 8% | The channel a post was forwarded from. The propagation signal. |
is_editedboolean | 5.3% | Being re-measured; 5.3% is currently an underestimate and is published as one. |
emailsstring[] | 3% | 3.0% overall averages news channels at 0% with job channels at 9.0%; one job channel measured alone was 35%. Both numbers are given because the average would mislead. |
pollobject | 0.2% | With `has_poll`. Verified with a positive case. |
buttonsobject[] | not measured | Inline buttons such as Apply or Contact. Verified present; the rate is not yet measured. |
subscribersinteger | 100% | Exact count on channel rows when subscriber lookup is on, alongside the rounded `subscribers_raw`. |
from_cacheboolean | 100% | With `fetched_at` and `data_age_hours`. |
Every key is always present. A field that exists but is empty comes back as explicit null, so a parser never has to guess.
Datasets
Different record types go to different datasets, so the main table never carries columns that are blank on most rows.
defaultOne row per post in messages and search mode, or one row per channel in channel-info mode.billedchannelsChannel metadata with the exact subscriber count, when subscriber lookup is on.billedDiagnostic rowsA channel that could not be read, with the reason it could not.never billed
Pricing
Pay per delivered result. Charges are applied as each row is produced rather than in a lump at the end, so an aborted run bills only for what it actually gave you.
| Event | Price | Notes |
|---|---|---|
apify-actor-startActor start | $0.00001 | Effectively free. A run that finds nothing costs you nothing. |
messagePost | $0.001 | One public post with its full text, view count, reactions, links and entities. Error rows are never charged. |
search-resultSearch result | $0.001 | One post matching your keyword. Stemming false positives are removed before delivery and are not charged. |
channelChannel record | $0.002 | One channel with its metadata and its exact subscriber count. |
Measurements
Each figure is shown with the method that produced it. A benchmark without a method is a marketing claim wearing a number's clothes.
Fill-rate sample
893 posts across 9 channels
No field is published without a measured rate, and the two fields that could not be measured say so rather than defaulting to a promise.
Search versus crawl
1 request and 24 KB against 25 requests and 712 KB
Same 20 matching posts from the same job channel, once through Telegram's search endpoint and once by reading the channel history.
Subscriber precision
`11.1M` displayed, `11143438` returned
One additional 4 KB page per channel. Both the rounded string and the exact integer are delivered.
Attachments found
0 in 893 posts across 29 channels
Searched for documents, voice notes, audio, stickers, locations and round videos. The public preview does not appear to render them at all, so those fields were removed rather than shipped always-false.
Emails by channel type
0% news, 9.0% job channels, 35% in one job channel alone
The 3.0% overall figure is an average across both kinds, which is why the split is published next to it.
Cross-language search
180 results across 13 channels for one technology term
The catalogue is mostly non-English, but technical terms stay in the Latin alphabet inside posts in other scripts. Every one of the 180 was a genuine posting for that technology.
What it will not do
Stated plainly so you can judge fit before spending anything.
- Only public channels. Never groups, never private channels, never invite links — those return a free diagnostic row instead.
- Attachments are not extracted at all: no documents, voice notes, audio, stickers, locations or round videos. After 893 posts across 29 channels there was not one positive case, so the public preview almost certainly does not render them. Fields that would always be `false` were removed rather than shipped.
- Discover mode searches a curated catalogue of verified channels that ships with the Actor. It does not reach channels outside that list, and the run log states how many were searched.
- The catalogue is global and mostly not in English: Telegram's largest job channels are in Arabic, Persian, Chinese and Russian. Searching for a technology name works across languages; searching for an English phrase like `remote` returns less than you would expect.
- Media URLs carry a token and expire. They are a reference, not a permanent link.
- `is_edited` at 5.3% is being re-measured and is currently an underestimate.
- Concurrency is capped at 5 on purpose, which is well inside polite crawling limits rather than as fast as the pages would allow.
Privacy
- The Actor returns the full text of public channel posts, which may include email addresses, phone numbers, @mentions and t.me handles the author wrote. Output may contain personal data and whoever runs it is the data controller for what they do with it.
- Set `includeContactFields` to false and those four columns come back empty. The post text is returned in full either way, because a post with its contacts stripped is still the post.
- It does not cross-reference authors between channels, does not enrich from outside sources, does not build profiles of people, and does not discover channels by following mentions.
- Cached pages are held for 7 days and archives for 90, both expiring automatically. Every row declares `from_cache`, `fetched_at` and `data_age_hours`.
- Data policy and removal requests: telegram.actorstack.dev, or privacy@actorstack.dev
See also the data removal process.
Frequently asked questions
Do I need a Telegram account, a phone number or a bot token?
Can it read private channels or groups?
Can I search without knowing any channel names?
Why are there two numbers for views and subscribers?
Are attachments included?
What am I not charged for?
Does the post text include personal data?
Guides for this Actor
- Scrape Telegram channelsA walkthrough of reading public Telegram channels anonymously: the four modes, when search beats reading a history, and what the anonymous preview does and does not render.
- The Telegram API questionThe Bot API cannot read a channel it is not in. MTProto needs a phone number and an api_id. The public web preview needs neither, and it is what a channel publishes to the open web.
- Search versus crawlReading a channel's history to find 20 posts about one term took 25 requests and 712 KB. Asking Telegram to filter took 1 request and 24 KB. Same 20 results.
- Exact subscriber countsEvery abbreviated figure Telegram displays is a rounded string. One extra 4 KB request returns the exact integer, and both numbers ship so the rounding stays auditable.
- Stemming false positivesTelegram's search stems words rather than matching substrings, so it returns morphological relatives of your term. Filtering them out afterwards is free, and it has to happen before billing.
- The attachment findingDocuments, voice notes, audio, stickers, locations and round videos never appeared once in the anonymous preview. Fields that would always be false were cut rather than shipped.
- Field referenceEvery field the Actor returns, with the rate it was actually filled on across 893 posts in 9 channels — including the two that vary enough by channel type that an average would mislead.
- Channel discoveryDiscover mode searches a curated catalogue of verified public channels by niche. What that catalogue is, what it is not, and why searching for a technology beats searching for an English phrase.
- Channel id versus handleA channel that renames itself breaks every dataset keyed on its handle. The internal id survives the rename, and it is on every row.
- Data protectionA public post can contain an email, a phone number and a handle, and returning it in full is what makes it useful and what makes it personal data. Where that leaves the person running the run.