# Telegram Channel Scraper — Search Without a Handle or Login (`oswaldocarabano/telegram-channel-scraper`) Actor

Scrape public Telegram channels without an account: full post text, view counts, reactions, links, inline buttons, exact subscriber counts and server-side keyword search. Or search a curated catalog of verified channels by niche. No login, no phone number, no bot token, no API key.

- **URL**: https://apify.com/oswaldocarabano/telegram-channel-scraper.md
- **Developed by:** [Oswaldo Carabano](https://apify.com/oswaldocarabano) (community)
- **Categories:** Social media, News, Lead generation
- **Stats:** 3 total users, 2 monthly users, 94.1% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

Read public Telegram channels without an account. Full post text, view counts,
reactions, links, inline buttons, exact subscriber counts, and keyword search that runs
on Telegram's side.

**No login. No phone number. No bot token. No API key. No cookies.**

That is not a limitation, it is the design. Scrapers that sign in with a phone number
get their account banned and stop working; this one reads the same public preview pages
Telegram serves to any anonymous visitor, so there is no session to expire and nothing
to ban.

***

### Three ways to use it

**1. You know the channels.** Put handles or URLs in `channels`. All of these mean the
same channel: `durov`, `@durov`, `t.me/durov`, `https://t.me/s/durov`.

**2. You know the keyword, not the channels.** Use `mode: "discover"` with
`searchTerms` and a `niche`. It searches a **curated catalog of verified public
channels** that ships with the Actor, so you do not need to know any channel names.

**3. You want reach numbers.** Use `mode: "channel-info"` for metadata and the exact
subscriber count, one row per channel.

***

### What makes this one different

**Keyword search runs on Telegram's side.** Reading the whole history of a job channel
to find 20 posts mentioning "react" took 25 requests and 712 KB. With keyword search it
is **1 request and 24 KB** for the same 20 results. You pay for relevant rows, not for
the crawl.

**Exact match is on by default, and it is free.** Telegram's search uses stemming, not
substring matching: a search for `hiring` will return a post that says *"hire a cab"*.
This Actor removes those false positives before delivery — and **you are not charged
for a discarded candidate.**

**Exact subscriber counts.** The channel preview shows a rounded `11.1M`. This Actor
fetches one extra 4 KB page and gives you `11143438`. You get both numbers, so you can
audit the conversion. Every abbreviated number is delivered as a pair: `views` and
`views_raw`.

**t.me handles, not just @mentions.** Telegram contacts are published two ways:
`@SomeHandle` and `https://t.me/SomeHandle`. Most scrapers only match the first. The
second is **more common**, and both get their own column.

**Real diagnostics instead of "not found".** When a channel cannot be read, the Actor
makes one extra 4 KB request to tell you *why*: the channel does not exist, or it is a
group, or a bot, or a user account, or a public channel with its web preview switched
off. **These rows are never charged.**

**The internal channel ID** (`telegram_channel_id`) is in every row. It survives a
channel renaming its `@username`, which the handle does not.

***

### Pricing

| Event | Price |
|---|---|
| Post | **$0.001** |
| Search result | **$0.001** |
| Channel record | **$0.002** |
| Run start | $0.00001 |

Lower per result than any Telegram Actor on Apify with real usage — and with fields
they do not return.

**Three things you are never charged for:**

1. **Error rows.** A channel that redirects, a timeout, a truncated response: you get
   the diagnostic row for free.
2. **Candidates discarded by exact match.** The stemming false positives above.
3. **Rows you already received.** If a run is interrupted and resumed, delivered rows
   are not billed twice.

Charging happens immediately after each row is delivered, never in a batch at the end.
If a run dies halfway, you paid for exactly what you received.

***

### Output fields, with their measured fill rates

Measured over 893 posts across 9 channels. **No field is promised here without a
measured fill rate.**

| Field | Fill rate | Notes |
|---|---|---|
| `datetime` | **100%** | ISO 8601, exact to the second |
| `telegram_channel_id` | **100%** | internal ID, survives a rename |
| `message_id`, `url`, `channel_username` | **100%** | |
| `text` | **99.6%** | full text, never truncated |
| `views` / `views_raw` | **99.2%** | typed value and the string as shown |
| `links` / `link_domains` | **80.7%** | read from the link target, not from the text |
| `has_photo` | 52.4% | |
| `tme_handles` | **43.2%** | 64.2% in job channels |
| `hashtags` | 37.4% | |
| `mentions` | 30.8% | 53.5% in job channels |
| `has_video` | 11.6% | |
| `reactions` / `reactions_total` | 11.0% | |
| `author` | 11.0% | only channels that sign their posts |
| `is_reply` / `reply_to_url` | 11.0% | |
| `forwarded_from` | 8.0% | |
| `is_edited` | 5.3% | ⚠️ being re-measured; this figure is an underestimate |
| `emails` | **3.0% overall / 9.0% in job channels** | see note below |
| `has_poll` / `poll` | 0.2% | verified with a positive case |
| `buttons` | verified, rate not yet measured | inline buttons like Apply or Contact |
| `subscribers` (channel rows) | **100%** | exact when `includeExactSubscribers` is on |

**About `emails`:** the 3.0% overall figure averages news channels (0%) with job
channels (9.0%), so both numbers are given. In one job channel measured on its own it
was 35%. Do not plan around the average.

***

### Known limits, stated up front

**Attachments are not extracted.** No documents, voice notes, audio files, stickers,
locations or round videos. After searching 29 channels and 893 posts there was not one
positive case: the public preview almost certainly does not render attachments at all.
A scraper that signs in with a phone number *can* download files, and this one cannot.
Fields that would always be `false` were removed rather than shipped.

**Only public channels.** Never groups, never private channels, never invite links. If
you pass one, you get a free diagnostic row explaining which it was.

**The catalog is a curated list.** Discover mode searches the verified public channels
that ship with the Actor, grouped by niche. The catalog is built and checked by hand,
and the run log tells you exactly how many channels were searched. It does not reach
channels outside that list.

**The catalog is global, and mostly not in English.** Telegram's largest job channels
are in Arabic, Persian, Chinese and Russian, and the catalog reflects that. In practice
this matters less than it sounds for technical searches, because technical terms stay in
the Latin alphabet even inside an Arabic post: a search for `react` returned 180 results
across 13 channels, every one of them a genuine React job posting. But a search for an
everyday English word like `remote` or `hiring` will return less than you might expect,
because the surrounding text is in another language. Search for the technology, the tool
or the company name rather than for the phrasing.

**Media URLs expire.** Telegram's CDN links carry a token and stop working after a
while. They are given as a reference, not a permanent link.

***

### Data protection

**This Actor returns the full text of public channel posts, plus any email addresses,
phone numbers, @mentions and t.me handles they contain. Output may include personal
data. You are the data controller for whatever you do with it.**

Set `includeContactFields` to `false` and those four columns come back empty. The post
text is always returned in full either way.

What this Actor does not do: it does not cross-reference authors between channels, does
not enrich anything from outside sources, does not build profiles of people, and does
not discover channels by following mentions. It returns what a channel published, to
the person who asked for that channel.

Removal requests: **privacy@actorstack.dev** · policy:
**https://telegram.actorstack.dev**

Cached pages are held for 7 days and archives for 90, both expiring automatically.
Every row tells you whether it came from cache (`from_cache`), when it was fetched
(`fetched_at`) and how old it is (`data_age_hours`). Set `maxCacheAgeDays` to `0` to
force a fresh fetch.

***

### Example input

Read the latest 200 posts from two channels:

```json
{
  "mode": "messages",
  "channels": ["durov", "https://t.me/s/tgbeta"],
  "maxMessagesPerChannel": 200
}
```

Find React jobs without knowing any channel names:

```json
{
  "mode": "discover",
  "niche": "jobs",
  "searchTerms": ["react", "typescript"],
  "maxChannels": 40
}
```

Reach metrics with exact subscriber counts:

```json
{
  "mode": "channel-info",
  "channels": ["durov", "telegram"],
  "includeExactSubscribers": true
}
```

# Actor input Schema

## `mode` (type: `string`):

messages: read a channel's posts. search: read only posts matching your keywords, filtered on Telegram's side. discover: search a curated catalog of verified channels by niche, so you do not need to know any channel names. channel-info: metadata and exact subscriber count only.

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

Public channel handles or t.me URLs. All of these work and mean the same channel: durov, @durov, t.me/durov, https://t.me/s/durov. Not needed in Discover mode.

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

Keywords to look for. Telegram filters on its side, which is far cheaper than reading a whole channel. Required in Search and Discover modes.

## `exactMatch` (type: `boolean`):

Telegram's search uses stemming, not substring matching, so a search for hiring can return a post that says hire a cab. With this on, those false positives are removed before delivery and you are never charged for them. Costs nothing and is on by default.

## `niche` (type: `string`):

Which part of the curated catalog to search. Use all to search every verified channel in the catalog.

## `maxChannels` (type: `integer`):

How many catalog channels to search, largest first. Each channel costs one request.

## `maxMessagesPerChannel` (type: `integer`):

A hard cap, so a large channel can never run up an unexpected bill. In Discover mode the default is 20 per channel instead, because that mode opens many channels at once.

## `newestFirst` (type: `boolean`):

Off walks the channel forward from the oldest post instead, which is resumable across runs.

## `sinceDate` (type: `string`):

ISO date, for example 2026-01-01. Walking newest-first stops as soon as it passes this date, so you are not charged for posts you did not want.

## `untilDate` (type: `string`):

ISO date, for example 2026-08-01.

## `includeExactSubscribers` (type: `boolean`):

The channel preview only shows a rounded count like 11.1M. This fetches one extra 4 KB page to get the exact number, 11143438. Ignored in Discover mode.

## `includeContactFields` (type: `boolean`):

Emails, phone numbers, @mentions and t.me handles found in the post text, each in its own column. Turn this off if you do not want them in your dataset. The post text itself is always returned in full either way.

## `maxCacheAgeDays` (type: `number`):

Pages already fetched recently are reused, which is faster and puts less load on Telegram. Every row tells you whether it came from cache and how old it is. Set to 0 to force a fresh fetch.

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

Deliberately capped at 5. This Actor stays well inside polite crawling limits.

## `proxyFallback` (type: `boolean`):

A safety net, off unless it is needed. Requests go direct, which costs nothing and is what works: no rate limiting has ever been observed on these pages. If Telegram starts refusing our requests, this retries through a proxy with a sticky session instead of silently returning fewer rows. Turn it off for a guaranteed zero proxy cost.

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

Not needed. This Actor reads public pages that Telegram serves to any anonymous visitor, and no rate limiting has been observed. Leave it off unless a run reports blocks.

## Actor input object example

```json
{
  "mode": "messages",
  "channels": [
    "durov"
  ],
  "searchTerms": [
    "react"
  ],
  "exactMatch": true,
  "niche": "all",
  "maxChannels": 40,
  "maxMessagesPerChannel": 200,
  "newestFirst": true,
  "includeExactSubscribers": true,
  "includeContactFields": true,
  "maxCacheAgeDays": 1,
  "maxConcurrency": 5,
  "proxyFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per post: full text, view count, reactions, links, hashtags, inline buttons, contact entities and the original post URL.

## `channels` (type: `string`):

One row per channel: title, description, exact subscriber count, internal channel id and content counters.

## `errors` (type: `string`):

One row per input that could not be read, with the reason. These rows are never charged.

# 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"
    ],
    "searchTerms": [
        "react"
    ]
};

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

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

```

## MCP server setup

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