DEV Community

Chorelet
Chorelet

Posted on Originally published at chorelet.app AI-assisted

Export any public Telegram channel to CSV or JSON in 5 minutes — no API keys, no phone number

Telegram's official API needs an api_id, an api_hash and a phone number tied to a real account — a lot of ceremony (and risk) if all you want is the posts of a few public channels. This post shows a lighter route: the public web preview that Telegram publishes for every open channel at t.me/s/<channel>, packaged as an API call.

What you get

One JSON object per post: channel, channelTitle, channelSubscribers, id, url, date (UTC), text, author, views, reactions (per emoji) and reactionsTotal, forwardedFrom, replyTo, mediaTypes, photos, videos, documents, poll, linkPreview, links, hashtags, mentions. Newest first, with a date cut-off so daily runs stay tiny.

1. One curl

Create a free account at apify.com (no card; $5 of usage per month included), grab the API token from Settings → Integrations, then:

curl -X POST "https://api.apify.com/v2/acts/chorelet~telegram-channel-posts-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN&format=csv" \
  -H "Content-Type: application/json" \
  -d '{"channels": ["telegram", "durov"], "maxPostsPerChannel": 100, "postedAfter": "30 days"}' \
  -o posts.csv
Enter fullscreen mode Exit fullscreen mode

format=csv → spreadsheet; drop it for JSON; xlsx also works. The call blocks until the run finishes (a few seconds for a couple of hundred posts).

2. Python

pip install apify-client
Enter fullscreen mode Exit fullscreen mode
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("chorelet/telegram-channel-posts-scraper").call(
    run_input={"channels": ["telegram", "durov"], "maxPostsPerChannel": 200, "postedAfter": "7 days"}
)
for post in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(post["date"][:10], post["channel"], post["views"], (post["text"] or "")[:80])
Enter fullscreen mode Exit fullscreen mode

3. Keep it running

In the Apify console: Schedules → daily → the same input with "postedAfter": "1 day". The Actor stops at the first post older than the cut-off, so a daily run over 20 channels is usually a few hundred posts — cents, or free on the free tier. Add an integration (Google Sheets, Slack, webhook) on the run's "Integrations" tab if you want the data pushed somewhere.

What it can't do

  • Private channels, invite links (t.me/+…), bots, groups without a web preview → the run reports "not a public channel" for that entry and continues with the rest.
  • Comments (discussion groups) aren't part of the preview.
  • Counts are the rounded numbers Telegram shows (1.2K → 1200). Media URLs are CDN links that expire.

How it works under the hood

The preview page is plain HTML: 20 posts per page, ?before=<id> for older ones, reactions and views as text, media as CDN links.

If you'd rather build your own, the selectors are simple:

  • each post is div.tgme_widget_message[data-post="Channel/123"]
  • inside it: time[datetime], .tgme_widget_message_text, .tgme_widget_message_views, .tgme_widget_message_reactions
  • the next page of history is the a.tme_messages_more[data-before] link

Three gotchas I hit:

  1. Usernames can be aliases. t.me/s/proglib renders the channel proglibrary — take the name from data-post, not from your input.
  2. Bots and channels with the preview disabled answer HTTP 200 with a normal-looking page but no .tgme_channel_info block. Check for the block, not the status code.
  3. Post #1 is the "Channel created" service message (class service_message) — skip those.

Disclosure

The Actor is mine. It's $0.50 per 1,000 posts after the free tier, and it's checked by an automated run every morning so it gets fixed when Telegram changes the markup: https://chorelet.app/p/telegram-channel-posts-scraper

Top comments (0)