Telegram Channel Scraper - Posts, Views, Forwards & Watchlists
Pricing
from $0.00005 / actor start
Telegram Channel Scraper - Posts, Views, Forwards & Watchlists
Extract public Telegram channel posts, view counts, forwards, reactions, media and outbound links — no bot token, no MTProto, no phone number. Adds a cross-channel mention graph, keyword watchlists across scheduled runs, and subscriber-delta change events.
Pricing
from $0.00005 / actor start
Rating
0.0
(0)
Developer
Eonix Pvt Ltd
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a month ago
Last modified
Categories
Share
Telegram Public Channel Intelligence — Posts, Views, Forwards & Watchlists
Extract everything a public Telegram channel exposes to the open web, and turn it into a monitored feed.
No bot token. No MTProto. No phone number. No session string. No login of any kind.
This actor reads https://t.me/s/{channel} — the fully server-rendered public preview that
Telegram serves to anyone — and parses it with Cheerio. There is no browser, no API key, and
nothing to authorize.
Why this one
A raw post dump is table stakes; several well-rated actors already do it. Three layers that none of them ship are the reason this exists:
| Layer | What it gives you |
|---|---|
| Cross-channel mention graph | Weighted, directed {source, target, weight, kinds} edges built from forwards and @mentions. Written to GRAPH.json. Shows who amplifies whom. |
| Incremental watchlists | Keywords and regexes matched on every scheduled run. New matches fire typed change records — a real alerting primitive, not a one-off search. |
| Subscriber-delta events | Growth and collapse detected across runs and emitted as typed change records when the move exceeds your threshold. |
Plus the thing this category gets wrong most often: when a channel has no public preview, this actor says so. See Reliability.
Free plan limits
Free-plan runs are capped to 1 channel, 25 messages, no AI, and no channel expansion, and the run logs:
Free-plan run: capped to 25 records. Paid plans run uncapped.
Paid plans run uncapped, up to your configured limits.
What a run costs you
Before any work begins, the run computes an estimate from your resolved input, logs it, and
writes it to the key-value store as COST-ESTIMATE.json.
| Event | When it fires | Price |
|---|---|---|
channel-resolved | Once per channel successfully fetched, after its record is pushed | $0.02 |
message-returned | Once per message pushed | $0.0004 |
watchlist-hit | Once per watchlist match | $0.005 |
ai-brief | Once per successful AI brief | $0.02 |
request-served | Standby HTTP requests only | $0.001 |
You are never charged for:
- a channel with no public preview (
status: "unreachable") - a channel whose markup drifted (
kind: "health") - a failed or malformed AI call
- an incremental run that finds nothing new (beyond the per-channel event)
Example — the shipped default input (2 channels × 200 messages, 2 keywords):
channel-resolved 2 x $0.020000 = $0.040000message-returned 400 x $0.000400 = $0.160000watchlist-hit 20 x $0.005000 = $0.100000ESTIMATED MAXIMUM TOTAL: $0.300000
That is an upper bound. With incremental: true (the default), later scheduled runs only
fetch new posts and typically cost a small fraction of it.
Input
| Field | Type | Default | Notes |
|---|---|---|---|
channels | string[] | ["telegram","telegramtips"] | Accepts @handle, t.me/x, t.me/s/x, a full URL, or a bare name |
maxMessagesPerChannel | integer | 200 | Max 5000 |
sinceDate | string | — | ISO date; stop walking once posts are older |
incremental | boolean | true | Only fetch posts newer than the last run (see "Runs are never empty") |
keywords | string[] | — | Case-insensitive plain-text terms |
regexes | string[] | — | JavaScript regexes; unsafe patterns are rejected |
expandMentions | boolean | false | Queue newly discovered channels |
maxDiscoveredChannels | integer | 0 | Ceiling for expansion |
subscriberDeltaPct | number | 1 | Threshold for a subscriber change event |
openaiApiKey | string (secret) | — | Enables one AI brief per channel |
model | string | gpt-4o-mini | Model for the brief |
proxyConfiguration | object | Apify Proxy |
Output
Every record carries the same envelope, shared across this portfolio:
{ kind, target, status, collectedAt, source, ...payload }
kind is record | change | summary | health.
status is ok | partial | unreachable | endpoint_unavailable | skipped.
Real sample output
Copied verbatim from a local run on 16 Aug 2026 — not hand-written.
Channel record
{"kind": "record","target": "telegram","status": "ok","collectedAt": "2026-08-16T16:56:58.762Z","source": "https://t.me/s/telegram","recordType": "channel","channel": "telegram","title": "Telegram News","description": "The official Telegram on Telegram. Much recursion. Very Telegram. Wow.","subscribers": 9810000,"photosCount": 14,"videosCount": 225,"linksCount": 372,"avatarUrl": "https://cdn1.telesco.pe/file/LvdKdKHGD-oGHuw...jpg","isVerified": true,"contentAdvisory": false}
Message record
{"kind": "record","target": "telegram","status": "ok","collectedAt": "2026-08-16T16:56:58.762Z","source": "https://t.me/s/telegram","recordType": "message","channel": "telegram","messageId": 454,"url": "https://t.me/telegram/454","datetime": "2026-07-19T17:58:20+00:00","text": "For all the details on these new features, check out our blog:\nhttps://telegram.org/blog/communities-editor-invisible-messages\n\nJuly Features\n1 • 2 • 3 • 4 • More","views": 1500000,"forwardedFromChannel": null,"forwardedFromName": null,"isForwarded": false,"replyToId": null,"hasMedia": false,"mediaTypes": [],"mediaUrls": [],"outboundLinks": ["https://telegram.org/blog/communities-editor-invisible-messages"],"mentionedChannels": [],"hashtags": [],"reactions": [],"totalReactions": 0,"edited": false,"editedAt": null,"authorSignature": null}
Watchlist hit
{"kind": "change","target": "telegram","status": "ok","collectedAt": "2026-08-16T16:56:58.762Z","source": "https://t.me/s/telegram","type": "watchlist_hit","channel": "telegram","messageId": 450,"url": "https://t.me/telegram/450","matched": "privacy","matchKind": "keyword","snippet": "…in a visual editor that supports dozens of formatting options and allows you to generate text with privacy-conscious AI tools. July Features 1 • 2 • 3 • 4 • More","views": 944000,"datetime": "2026-07-19T17:57:15+00:00"}
Runs are never empty
In incremental mode a run over a quiet channel legitimately finds nothing new — many channels post weekly or monthly, so a daily run would otherwise return an empty dataset and look broken.
When a run finds no new posts on any channel, it re-delivers the 25 most recent posts from
the first reachable channel and sets fallbackUsed: true on the run summary. Watchlists are
not re-evaluated for those posts: they were already matched on an earlier run, so firing the
same alerts again would spam you and charge you twice for one event.
Turn this off by setting incremental: false, which always walks history up to your message
limit.
Artifacts written to the key-value store
| Key | Contents |
|---|---|
COST-ESTIMATE.json | What this run is expected to charge you |
SUMMARY.json | Run totals, per-channel outcomes, charge counts |
GRAPH.json | Mention/forward graph: nodes and weighted edges |
UNIT-ECONOMICS.json | Developer-facing cost and margin measurement |
{channel}-brief.md | AI brief, when an OpenAI key is supplied |
Field notes
editedAtis alwaysnull. The public preview exposes only that a post was edited, never when. Rather than invent a timestamp,editedis a boolean andeditedAtis kept null for schema stability.htmlTextis sanitized. Telegram's own markup ships inlineonclickhandlers on every outbound link. Script/iframe elements,on*attributes andjavascript:/data:URLs are stripped so the field is safe to render.viewsmay benull. Absent is not zero, and the two are never conflated.mentionedChannelsexcludes self-mentions, which would otherwise dominate the graph.
Reliability
Measured during the build, 16 August 2026:
- 17 distinct public channel names tested.
- 9 exposed a public preview — all 9 parsed successfully (100%).
- 8 did not expose a preview — all 8 were correctly reported as
unreachable, with no charge, and the run still succeeded (100%). - No run failed. Full-history walks, incremental re-runs, standby requests and all-unreachable runs all completed successfully.
- Default input completes in ~24 seconds (400 messages, 2 channels).
- A repeat incremental run completes in ~3 seconds and logs
0 new messages.
The volume leader in this niche is rated 2.11/5 across 16 reviews precisely because it reports SUCCESS while returning nothing. That failure mode is designed out here:
- A page with no preview is detected explicitly (Telegram serves these with HTTP 200) and
reported as
unreachable. - A 2xx response whose shape no longer matches emits
SOURCE DRIFT: <source> returned unexpected shapeand akind: "health"record, so breakage is visible in your dataset rather than as a silently empty result. - If every source fails, the run still pushes health records and a summary. It never exits empty.
What this actor does NOT do
Honest coverage limits, so you can judge fit before you spend anything:
- It cannot read channels without a public preview. Many well-known names —
bbcnews,reuters,nasa,techcrunchamong those tested — do not serve one. They returnunreachableand cost you nothing, but no amount of retrying will produce their posts. - No private channels, no groups, no DMs, no member lists. There is no login, so anything behind one is out of reach by construction.
- No comment threads or discussion-group replies. Only in-channel reply references.
- No reaction-per-user data, only aggregate counts per emoji.
- No edit timestamps — see Field notes.
- No media file downloads. Media URLs are extracted; the bytes are not fetched.
- No historical view-count series. Views are whatever the page shows at collection time.
- Deleted posts are invisible, and gaps in message ids are not reconstructed.
- Not a real-time firehose. It is a polling actor; latency is your schedule interval.
Ethics and compliance
- Public channels only. This reads the same page any logged-out browser can load.
- No member lists, no private groups, no direct messages, no user profiling.
- No authentication is performed or required, so no account is impersonated.
- When a channel self-labels as restricted, the channel record carries
contentAdvisory: truerather than the content being silently filtered — you decide. - You remain responsible for how you use collected data, including GDPR obligations where posts contain personal data.
Use from an AI agent
Apify MCP. This actor is callable as a tool through the Apify MCP server. Point your MCP
client at https://mcp.apify.com and include this actor; the input schema above is the tool
schema.
Standby mode. When ACTOR_STANDBY_PORT is set, the actor stays warm and answers HTTP
requests, running the identical pipeline and charging the identical events plus
request-served:
GET https://<your-actor>.apify.actor/?channels=telegram,telegramtips&maxMessagesPerChannel=50&keywords=privacyGET https://<your-actor>.apify.actor/health
Query parameters mirror the input fields; list fields accept repeats or comma-separated values.
The response is the same JSON envelope, plus charged, economics and warnings.
openaiApiKey is deliberately ignored in standby mode — secrets must not travel in a query
string, where they land in access logs and browser history. Use a normal run for AI briefs.
Maintenance
- Broken-source issues are triaged within 2 business days, with a fix or a written workaround within 5 business days.
- Endpoint drift is detected automatically and surfaced in the dataset, so breakage is visible immediately rather than after a silent empty run.
- Report issues on the actor's Apify page.
Running it locally
npm installnpm run verify # lint + typecheck + 73 unit testsnpm run acceptance # live run against the shipped INPUT.json
Useful scripts:
| Script | Purpose |
|---|---|
npm run verify | Lint, type-check and the full unit suite |
npm run acceptance | Live run using INPUT.json, snapshots economics |
npm run acceptance:scaled | 10x-scaled run for cost-per-item comparison |
npm run acceptance:free | Exercises the free-plan caps |
npm run calibrate | Prints the pricing calibration report |
For the developer: unit-economics calibration
profit = (0.8 × revenue) − platform usage cost, so prices must be validated, not guessed.
Every run writes UNIT-ECONOMICS.json and logs a verdict.
Set your own rates. Read them off your Apify billing page
and put them in .env:
COST_CU_USD=0.25COST_PROXY_GB_USD=8.0
The values in .env.example are Apify's commonly published list rates, used as a documented
assumption so calibration produces an arguable number. They vary by plan and change over time —
verify them. If they are unset, platform cost resolves to $0 and the report correctly prints
CALIBRATION NOT PERFORMED — rates unset instead of a fake healthy margin.
Measured result (16 Aug 2026, local estimate at the rates above)
| Event | Hypothesis | Measured cost | Floor (3× cost) | Recommended |
|---|---|---|---|---|
channel-resolved | $0.020000 | $0.000825 | $0.003096 | $0.020000 |
message-returned | $0.000400 | $0.000054 | $0.000201 | $0.000400 |
Both spec prices survive calibration. message-returned sits at 2.0× its cost-covering
floor and channel-resolved at 6.5×. Run margin was 94.9%, verdict HEALTHY.
Scale comparison (default vs 10×):
| Scale | Driver events | Cost/event | Margin |
|---|---|---|---|
| default | 400 | $0.000054 | 94.9% |
| scaled | 869 | $0.000053 | 95.4% |
Cost per message fell as volume rose, so fixed cost amortizes correctly and there is no per-item fixed-cost problem the pricing needs to absorb.
On residential proxy. Residential is the default proxy tier and it is affordable here
because a page is ~134 KB and yields ~20 messages (~6.7 KB/message). Telegram serves this
surface uncompressed (no content-encoding), so the byte accounting equals real transfer
rather than under-counting it. The pay-per-event + usage toggle is deliberately not
enabled: Apify warns it reduces pricing transparency and hurts the quality score, and this
actor does not need it to be profitable.
License
MIT