# Telegram Channel Scraper — Posts & Stats (`insight.solutions/telegram-channel-scraper`) Actor

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.

- **URL**: https://apify.com/insight.solutions/telegram-channel-scraper.md
- **Developed by:** [Insight Solutions](https://apify.com/insight.solutions) (community)
- **Categories:** Social media, News, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.15 / 1,000 post returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Telegram Channel Scraper — Posts & Stats

**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

```json
{
  "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:

```jsonc
{
  "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:

```jsonc
{
  "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 `since` set 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.** `views` on every post, `subscribers` on every channel, `publishedAt` to 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. `links` and `linkPreview` give 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: 0` walks 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 `errorType` you 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.
- **`since` is 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.

```bash
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}'
```

```python
## pip install apify-client
from apify_client import ApifyClient

client = 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](https://apify.com/insight.solutions/youtube-transcript-api) — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- [YouTube Comments API](https://apify.com/insight.solutions/youtube-comments-api) — comments and replies with likes, pinned and hearted flags, newest or top sort.
- [YouTube Channel API](https://apify.com/insight.solutions/youtube-channel-api) — a channel's videos, Shorts and live streams, plus YouTube search.
- [Podcast Search, Episodes & Charts API](https://apify.com/insight.solutions/podcast-api) — Apple Podcasts search, charts and full episode feeds.
- [Bluesky Scraper](https://apify.com/insight.solutions/bluesky-scraper) — profiles, posts, followers and follows from the public AT Protocol API.
- [Substack Scraper](https://apify.com/insight.solutions/substack-scraper) — posts with full free text, comments and publication profiles.

**News, documents & the web**

- [Google News Search, Topics & Real Article URLs](https://apify.com/insight.solutions/google-news-api) — news search and topic feeds with the publisher's real URL decoded.
- [Website to Markdown — Content Extractor for LLMs & RAG](https://apify.com/insight.solutions/website-content-extractor) — any site as clean Markdown, text and heading-aware chunks.
- [Internet Archive API](https://apify.com/insight.solutions/internet-archive-api) — archive.org search, item metadata, files and reviews.
- [Wayback Machine Toolkit](https://apify.com/insight.solutions/wayback-toolkit) — archived URL inventories, snapshots and text diffs between dates.
- [Website Technology Detector](https://apify.com/insight.solutions/website-tech-detector) — the tech stack behind any site, with the evidence for each detection.
- [Domain Intelligence API](https://apify.com/insight.solutions/domain-intelligence-api) — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- [SEO Page Audit](https://apify.com/insight.solutions/seo-page-audit) — sitemap crawl with on-page checks, structured data and broken-link reports.
- [Keyword Suggestions API](https://apify.com/insight.solutions/keyword-suggestions-api) — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- [Website Contact Extractor](https://apify.com/insight.solutions/website-contact-extractor) — emails, phone numbers and social profiles from any list of websites.

**Business, finance & jobs**

- [Congress & Insider Trades API](https://apify.com/insight.solutions/congress-insider-trades-api) — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- [SEC EDGAR API](https://apify.com/insight.solutions/sec-edgar-api) — filings, XBRL financials and full-text search by ticker or CIK.
- [Y Combinator Companies, Batches & Founders](https://apify.com/insight.solutions/yc-companies-directory) — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- [Career Site Jobs API](https://apify.com/insight.solutions/ats-jobs-api) — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- [New Job Postings Monitor](https://apify.com/insight.solutions/job-postings-monitor) — new, closed and changed postings on the career sites you watch.
- [Shopify Products API](https://apify.com/insight.solutions/shopify-products-api) — any Shopify store's catalogue, variants, prices and stock signals.

**Apps & games**

- [App Store & Google Play Reviews API](https://apify.com/insight.solutions/app-reviews-api) — reviews from both stores with ratings, versions and developer replies.
- [App Store Top Charts & App Search API](https://apify.com/insight.solutions/app-charts-api) — Apple top charts by country and genre, plus app search and details.
- [Steam Reviews API](https://apify.com/insight.solutions/steam-reviews-api) — Steam reviews with playtime, helpfulness and game details.
- [Steam Game Data API](https://apify.com/insight.solutions/steam-store-stats-api) — prices, tags, review scores, live player counts and top charts.

# Actor input Schema

## `channels` (type: `array`):

One entry per public channel. Every spelling works and they all reach the same channel: the bare username (`durov`), the `@name` (`@durov`), a link (`https://t.me/durov`), a preview link (`https://t.me/s/durov`), or a link to one of its posts (`https://t.me/durov/527` — the post number is ignored here). Duplicates across those spellings are read, and billed, **once**. Private invite links (`t.me/+AbCdEf`) and internal links (`t.me/c/…`) cannot be read at all and get a free diagnostic row saying so.

## `maxPostsPerChannel` (type: `integer`):

How many posts to return for each channel, **newest first**. Telegram serves 20 posts per request, so 100 posts is 5 requests and 1,000 is 50. Set **0 for as deep as the preview will go** — that is `maxPagesPerChannel` × 20 posts, 1,000 by default. There is no way to reach the very first post of a large channel: Telegram's public preview stops serving older pages long before that.

## `since` (type: `string`):

Stop as soon as a post older than this is reached — `2026-08-01`, or a full timestamp like `2026-08-01T12:00:00Z`. Because posts come back newest first, this is a **stop**, not a filter: a cutoff of yesterday costs one request per channel however large the channel is. That makes it the right setting for a scheduled run that only wants what is new. Leave empty for no cutoff.

## `includeChannelInfo` (type: `boolean`):

Write one `channel` row per channel before its posts: title, `@username`, description, subscriber count, verified flag, avatar URL, and Telegram's own counts of the photos, videos, links and files the channel has posted. It is free to fetch — the information is in the header of the page the posts come from — but it is a separately priced row, so it can be turned off for a run that only wants posts.

## `postUrls` (type: `array`):

Links to individual posts — `https://t.me/durov/300` — read one at a time through Telegram's embed widget. Use this for posts that have scrolled out of a channel's recent history, or when you have a list of specific posts and do not want the channel around them. One row each, and no `channel` row. A post listed here is fetched even if the same post also arrives from its channel's feed.

## `includeHtml` (type: `boolean`):

Add a `textHtml` column holding the post exactly as Telegram marked it up — `<b>`, `<i>`, `<a href>`, `<code>`, custom emoji tags and all. The plain-text `text` column is always there; this is for when you want the formatting or the anchor markup. It makes rows several times larger and it is off by default.

## `maxPagesPerChannel` (type: `integer`):

A hard ceiling on requests per channel, whatever `maxPostsPerChannel` says. Each page is 20 posts and roughly 140 KB, so the default of 50 is 1,000 posts and about 7 MB per channel. This is the safety rail for `maxPostsPerChannel: 0`, and the number to raise when you genuinely want to walk a channel's whole readable history.

## `maxConcurrency` (type: `integer`):

How many channels are read at once. Each parallel worker keeps its own proxy session, so one rate-limited channel burns only its own exit IP. Pages within one channel are always sequential and spaced 300–700 ms apart, because that is what paging politely looks like.

## `maxRunSecs` (type: `integer`):

Wall-clock budget for the whole run. When it is reached the Actor stops fetching, keeps and bills for every row it already wrote, and files a free diagnostic row for each channel it never reached or could not finish. A channel stopped part-way keeps its posts. Nothing is charged for work that did not happen.

## `proxyConfiguration` (type: `object`):

Telegram's public preview answers datacenter addresses, so the default is Apify's datacenter proxy — much cheaper than residential, and the proxy cost is already inside the per-row price. Switch to residential if you are reading many channels on a schedule and start seeing `blocked` rows: Telegram rate-limits by address, and sessions rotate automatically when an exit IP is refused.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "@telegram",
    "https://t.me/s/durov"
  ],
  "maxPostsPerChannel": 50,
  "since": "2026-08-01",
  "includeChannelInfo": true,
  "postUrls": [
    "https://t.me/durov/300",
    "https://t.me/telegram/441"
  ],
  "includeHtml": false,
  "maxPagesPerChannel": 50,
  "maxConcurrency": 4,
  "maxRunSecs": 240,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One row per post — text, publish date, views, media URLs, links and link preview — plus one row per channel with its subscriber count and description. Channels that could not be read get a free diagnostic row saying why. Delivered as JSON items in the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "channels": [
        "durov",
        "telegram"
    ],
    "maxPostsPerChannel": 50,
    "since": "",
    "includeChannelInfo": true,
    "postUrls": [],
    "includeHtml": false,
    "maxPagesPerChannel": 50,
    "maxConcurrency": 4,
    "maxRunSecs": 240,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("insight.solutions/telegram-channel-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "channels": [
        "durov",
        "telegram",
    ],
    "maxPostsPerChannel": 50,
    "since": "",
    "includeChannelInfo": True,
    "postUrls": [],
    "includeHtml": False,
    "maxPagesPerChannel": 50,
    "maxConcurrency": 4,
    "maxRunSecs": 240,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("insight.solutions/telegram-channel-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "channels": [
    "durov",
    "telegram"
  ],
  "maxPostsPerChannel": 50,
  "since": "",
  "includeChannelInfo": true,
  "postUrls": [],
  "includeHtml": false,
  "maxPagesPerChannel": 50,
  "maxConcurrency": 4,
  "maxRunSecs": 240,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call insight.solutions/telegram-channel-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,insight.solutions/telegram-channel-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/hCxdEZKQeMobyYWod/builds/fVENE4bTzHJPq9vfP/openapi.json
