# Telegram Channel Scraper - Verified Posts (`marekhartmann/telegram-channel-scraper`) Actor

Scrape public Telegram channels without an API key or bot token: posts, views, reactions, media URLs, forwards, hashtags. Never reports an unreadable channel as empty - it fails with a reason, so you never pay for a silent empty run. No member lists, no personal data.

- **URL**: https://apify.com/marekhartmann/telegram-channel-scraper.md
- **Developed by:** [Marek Hartmann](https://apify.com/marekhartmann) (community)
- **Categories:** Social media, Automation, Open source
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 — verified results or an explicit error

[![tests](https://github.com/marekhartmann-creator/telegram-verified-scraper/actions/workflows/tests.yml/badge.svg)](https://github.com/marekhartmann-creator/telegram-verified-scraper/actions/workflows/tests.yml)

Scrape **public Telegram channels** without an API key, a bot token or a phone
number: posts, views, reactions, media URLs, forwards, hashtags and mentions,
with date and keyword filtering.

The difference from every other Telegram scraper on this Store is one rule:

> **An empty result is never used to report a problem, and a run that returns
> nothing costs you nothing.**

### Why that matters

Telegram answers `HTTP 200` for channel handles that do not exist. The response
is a normal-looking page with no channel content in it. A scraper that only
counts posts on that page sees zero, calls the run a success, and bills you for
it. One typo in a channel name and you get a clean, empty, paid-for dataset.

The same silent failure happens when a channel exists but its web preview is not
served, when Telegram restricts the channel, when the handle is a group or a user
profile instead of a channel, and when a page fails halfway through pagination.

This Actor classifies the channel **before** it trusts any post count:

| State | What it means | What you get |
|---|---|---|
| `PUBLIC_PREVIEWABLE` | Real channel, readable | Posts |
| `EXISTS_NO_PREVIEW` | Real channel, Telegram serves no web preview | Explicit error |
| `PRIVATE` | Invite-only | Explicit error |
| `RESTRICTED` | Telegram serves no public page for the handle | Explicit error |
| `NOT_A_CHANNEL` | Handle is a user, bot or group | Explicit error |
| `NOT_FOUND` | Nothing at this handle (usually a typo) | Explicit error |
| `UNREACHABLE` | Telegram could not be reached | Explicit error |

Every one of those states except `PRIVATE` is checked against live Telegram by
`scripts/smoke.py`, which fails the build when a handle stops classifying the way
it did when the signature was captured.

On top of that, every channel gets a verification report:

- `verdict` — `OK`, `EMPTY_VERIFIED`, `PARTIAL` or `FAILED`
- `firstPostId` / `lastPostId` / `idGaps` / `coverageRatio` — how complete the
  history actually is (post ids are **not** contiguous; deleted and service
  messages leave holes, and a scraper that paginates by arithmetic walks past
  real posts)
- `reachedEndOfHistory`, `stoppedBy`, `pageFailures`

`EMPTY_VERIFIED` is only ever emitted when the channel header rendered, the
history container rendered, and the channel's own metadata does not contradict
the empty result.

### What this Actor deliberately does NOT do

**No members, no user profiles, no phone numbers, no personal data of any kind.**
Channel member lists are personal data under GDPR and their extraction conflicts
with Telegram's Terms of Service. This Actor reads public broadcast content only.
If you need member lists, this is the wrong tool — on purpose.

### Input

```json
{
  "channels": ["durov", "https://t.me/telegram"],
  "maxPostsPerChannel": 100,
  "minDate": "2026-01-01",
  "searchTerms": ["release", "update"],
  "includeMediaUrls": true,
  "maxConcurrency": 5,
  "failOnUnreadableChannel": true
}
```

Channels are fetched in parallel (`maxConcurrency`, 1-10). Telegram round-trips
dominate the wall clock, and compute time is billed, so waiting serially would be
your money spent on latency.

`failOnUnreadableChannel` (default **on**) ends the run as `FAILED` with a reason
when any requested channel could not be fully read. Turn it off to accept partial
results — the reports still tell you exactly what was missed.

### Output

One dataset item per post:

```json
{
  "channel": "durov",
  "postId": 385,
  "url": "https://t.me/durov/385",
  "datetime": "2026-08-01T10:00:00+00:00",
  "text": "…",
  "views": 1200000,
  "author": null,
  "isForwarded": false,
  "forwardedFrom": null,
  "isReply": false,
  "isEdited": false,
  "hashtags": ["telegram"],
  "mentions": [],
  "links": ["https://example.com"],
  "media": [{ "type": "photo", "url": "https://cdn.telegram.org/…" }],
  "linkPreview": { "title": "…", "url": "…" },
  "reactions": [{ "emoji": "👍", "count": 12 }],
  "reactionsTotal": 15
}
```

The dataset holds **only posts** — one clean row each, and the only thing you are
charged for. The verification reports (per-channel state, verdict, id coverage,
page failures) are written to the key-value store as `RUN_SUMMARY` on every run,
including runs that fail.

### Pricing

**Free while this Actor is in early access.** You only pay Apify platform usage
(compute), and that bill is deliberately small: this Actor talks plain HTTP to
Telegram's server-rendered pages instead of driving a headless browser, so it
runs in a fraction of the memory a browser-based scraper needs.

Pay-per-result pricing will be introduced later. When it is, the rule stays the
same as the promise above: **no start fee, and you are charged only for posts
that passed verification.** A run that returns nothing will never cost you
anything.

### Use cases

News and OSINT monitoring, brand and competitor tracking, crypto signal channel
archiving, market and disinformation research, dataset building.

### Need a scraper for something else?

I build custom Apify Actors and browser automation the same way this one is built:
a verified result or an explicit failure, never an empty dataset sold as a success.

- My other Actors: https://apify.com/marekhartmann
- Code and test suites: https://github.com/marekhartmann-creator

Tell me the site and what you need out of it.

# Actor input Schema

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

Channel handles or t.me links, e.g. durov, @durov, https://t.me/durov. Private channels, groups and user profiles are not supported by design.

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

Hard cap on posts per channel. 0 means the whole readable history (can be expensive).

## `minDate` (type: `string`):

ISO date, e.g. 2026-01-31. Pagination stops as soon as older posts appear.

## `maxDate` (type: `string`):

ISO date, e.g. 2026-08-01.

## `searchTerms` (type: `array`):

Keep only posts whose text contains at least one of these terms (case-insensitive). Leave empty to keep everything.

## `includeMediaUrls` (type: `boolean`):

Photo, video, voice, document and sticker URLs.

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

Adds the original HTML of every post. Off by default: it roughly doubles the size of each result.

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

How many channels to fetch at the same time (1-10). Higher is faster and cheaper in compute, but hits Telegram harder.

## `failOnUnreadableChannel` (type: `boolean`):

ON (recommended): if any channel is missing, private, restricted or only partially loaded, the run ends as FAILED with a reason instead of quietly returning fewer posts. OFF: partial results are accepted and the problem is reported in the run summary only.

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

Optional. Datacenter proxies are usually enough.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "telegram"
  ],
  "maxPostsPerChannel": 25,
  "includeMediaUrls": true,
  "includeHtml": false,
  "maxConcurrency": 5,
  "failOnUnreadableChannel": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `posts` (type: `string`):

One item per post that passed verification. A post is only written here when the channel it came from was confirmed readable.

## `runSummary` (type: `string`):

Verdict (OK, EMPTY\_VERIFIED, PARTIAL, FAILED), channel state, post id coverage and any page failures - for every requested channel.

# 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": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("marekhartmann/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": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("marekhartmann/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": 25
}' |
apify call marekhartmann/telegram-channel-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,marekhartmann/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/e1fQgm7ejlsLKTbDj/builds/neHE8o5zvjwFunVPx/openapi.json
