# Telegram Channel Scraper & Keyword Search — No Login (`tugelbay/telegram-posts-scraper`) Actor

Telegram scraper for public channel posts: read whole channels or search the channels you list by keyword. No bot token, phone login or API credentials. Text, views, dates, media URLs, links and the matched keyword. Schedule it for new posts only. JSON, CSV or API. $1.20–$2.00 per 1,000 posts.

- **URL**: https://apify.com/tugelbay/telegram-posts-scraper.md
- **Developed by:** [Tugelbay Konabayev](https://apify.com/tugelbay) (community)
- **Categories:** Social media, AI
- **Features:** Limited permissions
- **Stats:** 174 total users, 40 monthly users, 99.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 telegram posts

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Public Telegram scraper for `t.me/s/` channel previews: no bot token and no phone login. Save an Apify Task with `incremental: true` and a stable `monitorKey`, then attach a Schedule every 15–60 minutes. The first run stores a cursor and returns a recent sample; later runs return only unseen posts.

> **Fast first run** — start with the quick sample below. It reads public `t.me/s/<channel>` preview pages only.
> **No Telegram account, bot token, or phone number.** Private channels, groups, bots, and direct messages are out of scope.
> **Channel monitor and RAG** — verify a small sample, then schedule incremental runs. Coverage and revenue are not guaranteed.

For implementation notes, examples, and Telegram monitoring workflows, see the <a href="https://konabayev.com/tools/telegram-posts-scraper/?utm_source=apify_readme&utm_medium=referral&utm_campaign=telegram-posts-scraper" rel="nofollow sponsored">Telegram Posts Scraper guide</a> on Konabayev.com.

<a href="https://apify.com/tugelbay/telegram-posts-scraper">
  <img src="https://api.apify.com/v2/key-value-stores/bplRdpnd85eGQkW1N/records/telegram-posts-scraper-hero.png" alt="Telegram Posts Scraper overview: public channel posts without Telegram account credentials" width="100%">
</a>

<p>
  <img src="https://api.apify.com/v2/key-value-stores/bplRdpnd85eGQkW1N/records/telegram-posts-scraper-input-output.png" alt="Telegram Posts Scraper input and output example" width="49%">
  <img src="https://api.apify.com/v2/key-value-stores/bplRdpnd85eGQkW1N/records/telegram-posts-scraper-dataset-preview.png" alt="Telegram channel post dataset preview" width="49%">
</p>

### Keyword search inside channels

Add `keywords` to return only the posts that mention a word or phrase, instead of a whole channel feed. The Actor uses Telegram's own public channel search on `t.me/s/<channel>?q=<keyword>`, so there is still no account, bot token, or phone login.

```json
{"channels":["durov","telegram"],"keywords":["privacy","stories"],"maxItems":20}
```

- Each keyword is searched in each listed channel. Every row carries `matched_keyword`.
- A post that matches several keywords is delivered and charged once.
- Matching is Telegram's: it ignores letter case, works with non-Latin text, and may match word forms or text in a link preview. The Actor does not re-filter the text.
- This searches only the channels you list. It does not search all of Telegram and does not discover new channels.
- You pay per delivered post, the same price as without keywords. A keyword with no matches delivers and charges nothing.

**Keyword monitor** — save this as a Task and add a Schedule to get only new mentions:

```json
{"channels":["durov","telegram"],"keywords":["privacy"],"maxItems":50,"incremental":true,"monitorKey":"brand-mentions","outputFormat":"minimal"}
```

Each channel and keyword pair keeps its own cursor, so adding a keyword later does not replay or hide posts for the others.

### Channel Monitor (scheduled — new posts only)

This Actor already supports incremental monitoring of public preview pages. You do not need a second product.

1. Create an Apify **Task** with your public channel list.
2. Set `incremental: true` and a stable `monitorKey` (for example `daily-competitors`).
3. Attach a **Schedule** every 15–60 minutes, or hourly.
4. The first run returns a recent sample and stores the cursor. Later runs with the same Task and `monitorKey` return **only unseen posts**. You pay for delivered posts, not a full history replay.

If more than 1,000 posts arrive between runs, a fetch fails, or the public preview ends before the saved boundary, that channel emits no rows and its cursor does not advance. Cursor rules are in [Scheduled monitoring without manual cursors](#scheduled-monitoring-without-manual-cursors).

#### Example inputs

**Quick sample**

```json
{"channels":["durov","telegram"],"maxItems":10,"outputFormat":"full"}
```

**Channel Monitor** — save this as a Task, then add a Schedule:

```json
{"channels":["durov"],"maxItems":50,"incremental":true,"monitorKey":"daily-brand-watch","outputFormat":"minimal","proxyConfiguration":{"useApifyProxy":true}}
```

**RAG output**

```json
{"channels":["telegram"],"maxItems":30,"outputFormat":"rag"}
```

#### Pricing FAQ

**Is there a start fee?** Yes. **Actor Start** is **$0.00005 per GB** of run memory, with a minimum of one GB-event (Store snapshot **2026-07-14**). At the 1 GB default that is near zero next to per-post charges.

You pay per delivered **Telegram Post**: **$0.002** on Free, **$0.0018** on Bronze, **$0.0015** on Silver, and **$0.0012** on Gold and higher (same snapshot). A run that delivers no posts charges about the start fee only. Check the Pricing tab before a large run.

#### Honest limits

- Public channels with web previews only. Private channels, groups, bots, and direct messages are out of scope.
- Use Apify proxy for large runs. The default input already sets `useApifyProxy: true`.
- Recent public terminal runs succeeded about **98.7%** of the time (**393 of 398**, snapshot **2026-09-01**). That rate is historical. It is not a guarantee for your channels and it is not a revenue promise.

Scrape posts from public Telegram channels without phone numbers, 2FA, OAuth, bot tokens, or spending weeks on MTProto client libraries. This actor wraps Telegram's own `t.me/s/` SEO preview pages behind a single Apify-platform input with proxy rotation, pagination, filters, and a clean canonical output schema.

Perfect for **brand monitoring, channel intelligence, AI RAG pipelines, crypto/news tracking, research datasets**, and **competitive analysis** across one-person blogs to million-subscriber broadcast channels. Returns structured JSON with post text, view counts, timestamps, media URLs, link previews — plus an optional **clean Markdown format** that plugs straight into LangChain document loaders or Claude MCP tools.

### Русская инструкция: парсер Телеграм для публичных каналов

Этот парсер Телеграм выгружает посты из публичных каналов через страницы `t.me/s/` без номера телефона, бот-токена и Telegram API credentials. На выходе — JSON с текстом, датой, просмотрами, ссылкой на пост, медиа и link preview. Actor работает только там, где Telegram показывает публичный preview; закрытые каналы, группы и скрытые публикации недоступны.

#### Быстрый запуск

1. Укажите username канала без `@`, например `durov`.
2. Оставьте `maxItems: 5` для дешёвой проверки покрытия.
3. Выберите `full` для всех полей, `minimal` для компактного JSON или `rag` для Markdown, готового к загрузке в RAG-пайплайн.

```json
{
  "channels": ["durov"],
  "maxItems": 5,
  "outputFormat": "full"
}
```

Каждый сохранённый пост создаёт один dataset-item PPE event. Проверенная 2026-07-14 цена — `$0.002` за пост на FREE и `$0.0012` на GOLD и более высоких тарифах, плюс небольшой Actor-start event. Перед большой выгрузкой проверьте актуальную цену в Store. Режимы, фильтры и лимит `maxItems` одинаковы для бесплатного и платного плана; месячной квоты актора нет. Сохраняются лимит расходов запуска и общий бюджет сканирования.

#### Что вы получите и как автоматизировать

- `channel`, `post_id`, исходный URL и UTC-дата;
- текст, просмотры, фото, видео, документы и данные link preview, когда они есть в публичной странице;
- фильтры по минимальным просмотрам и дате;
- инкрементальный режим для сохранённого Apify Task: включите `incremental`, задайте стабильный `monitorKey` и подключите Schedule, чтобы получать только новые посты.

Готовый русскоязычный пример с безопасным лимитом доступен как публичный Task: [Парсер Телеграм каналов — без токена](https://apify.com/tugelbay/telegram-posts-scraper/examples/scrape-telegram-without-bot-token). Для приватных каналов, комментариев и полного архива при отключённом preview нужен другой, авторизованный Telegram-клиент — этот Actor такие данные не обещает.

### How to run and how it works

1. Start with one public channel and 10-25 posts.
2. Verify post URLs, UTC `date`, text/media coverage, views, and any block/error state.
3. Scale or schedule only after the public preview coverage fits the monitoring workflow.

Three input sources, used together in one run:

1. **`channels`** — list of channel handles (without `@` or `t.me/`). Scraper walks `t.me/s/<handle>` newest-first, paginating via `?before=<post_id>` until `maxItems` or end of feed. This is the main mode.
2. **`postUrls`** — list of full post URLs (e.g. `https://t.me/durov/478`). Each URL fetches one post with its OG metadata and embedded widget data.
3. **`search`** — channel handle lookup via `t.me/s/<query>`. Useful when you know a likely public handle but want to run it through the same preview parser.

Add **`keywords`** to the `channels` source to keep only matching posts; see [Keyword search inside channels](#keyword-search-inside-channels).

Post URLs must identify public posts with a positive numeric ID; private `t.me/c/...` routes and malformed paths fail before source work. Equivalent post URLs are normalized. Across post URLs, channels and handle lookup, the first successfully delivered row for a channel/post identity is retained; duplicate observations do not create another paid row or merge later field variants.

Explicit HTTP access/proxy refusals stop new requests across all modes, including embed fallback. A run with no delivered posts after failed acquisition fails with `SOURCE_UNAVAILABLE`; a successful public-preview response with no visible posts can still finish empty. Setup uses the remaining platform deadline when available, preserving the existing finish reserve.

`RUN_REPORT` contains delivered counts, filters, duplicates, source counts, execution phase, observed HTTP status, safe failure classification and incremental outcomes. `sourceRequests` counts logical client GET attempts; redirect hops are not counted separately. `sourceResponses` retains its HTTP 200-only meaning; `httpResponses` counts returned logical GET responses of any status, including refusals, with redirect hops excluded. Reports and `TELEGRAM_DIAGNOSTIC` summaries exclude channel identities, messages, URLs, cursor keys, source bodies, credentials and raw exception messages.

SDK persist-state, migration and graceful-abort events can save an in-progress checkpoint with `isFinal: false`. `platformEvent` records a notification, not proof that the run ended or failed. A normal final snapshot has `isFinal: true`; an actual Python cancellation records `CANCELLED`. `observedAt` identifies the snapshot time; counters and `snapshotSequence` are scoped to the current process. Writes are serialized and bounded by the remaining deadline, at most two seconds for event checkpoints and five seconds for the final report. A warning-level diagnostic summary remains available if report storage fails or times out. No extra Telegram requests are made for diagnostics.

Posts are pushed to the dataset **incrementally**. Rows already delivered remain in the dataset after interruption. A hard kill, out-of-memory termination or failure before SDK initialization can prevent pending writes or diagnostics from being saved; a checkpoint cannot establish the cause of every platform `ABORTED` status. Existing cursor safety rules still apply.

In ordinary channel and handle-lookup mode, posts are selected **newest first within each channel**, and filters apply before the delivered-post limit. Channels are processed in your input order; `maxItems` is shared across sources, not a per-channel allowance. Filtered scans inspect at most five times the remaining item limit, with a minimum of 100 and a maximum of 10,000 posts per channel. A warning identifies a scan that reaches this bound before filling the requested output. For scheduled incremental mode, the cursor rules and oldest-unseen-first delivery below apply.

> **What "public" means on Telegram:** every channel with previews enabled. The channel admin sets this toggle in Telegram → Channel Settings. Most news, crypto, and broadcast channels have previews on. Some private or sensitive ones don't — we detect this case and surface it cleanly (empty dataset with a warning log).

### Scheduled monitoring without manual cursors

Turn on **Only new posts on scheduled runs** (`incremental: true`) when saving a channel run as an Apify Task:

1. Set `channels`, `maxItems`, and optionally a stable `monitorKey` such as `daily-competitors`.
2. Run the Task once. This seeds a private cursor and returns the latest sample, oldest to newest within each channel.
3. Attach an Apify Schedule to that Task. Each later run with the same caller, monitor key, and channel returns only unseen posts, oldest unseen first within that channel.
4. Each Apify Task is isolated automatically. Change `monitorKey` when intentionally starting a fresh baseline or keeping multiple monitor states inside one Task.

```json
{
  "channels": ["durov", "telegram"],
  "incremental": true,
  "monitorKey": "daily-product-news",
  "maxItems": 100,
  "outputFormat": "minimal"
}
```

Cursor records use opaque SHA-256 keys scoped by caller, Apify Task, monitor name, and channel; those raw values are absent from the cursor record and key. Raw caller IDs, Task IDs, and monitor names are not logged. State advances only after successful dataset delivery. If a state write fails, a later run may repeat posts instead of skipping them.

Safety is fail-closed on later runs: if more than 1,000 posts arrive between runs, a fetch fails, the public preview ends before the saved boundary, pagination repeats a page without new posts, or stored state is malformed, that channel emits no rows and its cursor does not advance. Repeated pages stop the scan instead of issuing unbounded requests. Run a one-time non-incremental backfill, then choose a new `monitorKey` to seed a fresh baseline. The `before` input is ignored in incremental mode. `postUrls` and `search` still run normally, without cursor tracking.

Delivery is at-least-once. Cursor writes preserve any newer cursor already visible in state, but a failed state write or tightly overlapping runs can still repeat a post. Downstream automations should deduplicate on `channel` + `post_id`, and schedules should keep the same Task from normally overlapping.

Filters are evaluated when a post is first observed. A post skipped by `minViews` or `sinceIso` is not revisited later under the same monitor key.

### Why use this instead of alternatives?

| Feature                                | Telegram Posts Scraper        | Telethon (self-host)     | Telegram Bot API           | Other Apify scrapers |
| -------------------------------------- | ----------------------------- | ------------------------ | -------------------------- | -------------------- |
| **Auth required**                      | None                          | Phone number + 2FA + SIM | Bot token + channel admin  | Varies               |
| **Can scrape without joining channel** | Yes (public preview)          | Yes (after join)         | Only if bot is admin       | Varies               |
| **IP rotation**                        | Yes — Apify proxy             | Manual                   | N/A                        | Varies               |
| **Output formats**                     | full / minimal / rag-markdown | Custom code              | Raw objects                | full only            |
| **Link preview extraction**            | Yes, structured               | Manual                   | Manual                     | Rare                 |
| **View counts + dates**                | Yes                           | Yes                      | Not directly exposed       | Varies               |
| **Pagination**                         | Automatic via `?before=<id>`  | Manual                   | Manual                     | Varies               |
| **MCP compatible**                     | Yes (PPE = agent-friendly)    | No                       | No                         | Rarely               |
| **Pay model**                          | PPE — only what you scrape    | Free but self-host       | Free but bot must be admin | Varies               |

**Key advantage:** no Telegram account. No SIM risk. No `api_id`/`api_hash` registration. No 2FA loops. Just channel handles in, structured posts out.

### Input examples

#### Read two channels (default prefill)

```json
{
  "channels": ["durov", "telegram"],
  "maxItems": 10,
  "outputFormat": "full"
}
```

#### Monitor a crypto announcement channel since a specific date

```json
{
  "channels": ["binance_announcements"],
  "sinceIso": "2026-04-01T00:00:00Z",
  "minViews": 5000,
  "maxItems": 200
}
```

#### Fetch a specific viral thread + related posts

```json
{
  "postUrls": ["https://t.me/durov/478", "https://t.me/durov/479"],
  "outputFormat": "rag"
}
```

#### Paginate deep into a channel's history

```json
{
  "channels": ["telegram"],
  "before": 400,
  "maxItems": 500,
  "outputFormat": "minimal"
}
```

#### Look up a known public channel handle

```json
{
  "search": "telegram",
  "maxItems": 20
}
```

### Input parameters

| Field                | Type          | Default                 | Description                                                            |
| -------------------- | ------------- | ----------------------- | ---------------------------------------------------------------------- |
| `channels`           | array\[string] | `["durov", "telegram"]` | Channel handles (no `@`, no `t.me/`). Most common mode.                |
| `keywords`           | array\[string] | `[]`                    | Return only posts matching these words or phrases inside `channels`. Up to 20. |
| `postUrls`           | array\[string] | `[]`                    | Specific `https://t.me/<channel>/<id>` URLs to fetch.                  |
| `search`             | string        | —                       | Exact public handle lookup via `t.me/s/<handle>`; not a search by topic. |
| `maxItems`           | integer       | 10                      | Cap on total posts across all sources. Each is one PPE event.          |
| `before`             | integer       | —                       | Start at posts OLDER than this post ID. Default is newest first.       |
| `incremental`        | boolean       | `false`                 | For scheduled channel Tasks, return only posts unseen by this monitor. |
| `monitorKey`         | string        | `default`               | Stable cursor namespace inside one automatically isolated Apify Task.  |
| `outputFormat`       | enum          | `full`                  | `full` / `minimal` / `rag`. See Output format section.                 |
| `minViews`           | integer       | 0                       | Drop posts below N views. Good for "only viral posts" runs.            |
| `sinceIso`           | string        | —                       | ISO-8601 (`2026-01-01T00:00:00Z`). Older posts are skipped.            |
| `proxyConfiguration` | object        | `{useApifyProxy: true}` | Apify proxy group. Default auto-selects.                               |

### Output format

#### Full (default)

Every post includes:

```json
{
  "channel": "durov",
  "post_id": 478,
  "data_post": "durov/478",
  "url": "https://t.me/durov/478",
  "date": "2026-04-23T17:12:59+00:00",
  "text": "🔄 Your massive monthly Telegram update is out.\n\n🧠 100% private AI Editor for your outgoing messages...",
  "views": 1500000,
  "forwarded_from": null,
  "reply_to_url": null,
  "photos": ["https://cdn4.telesco.pe/file/..."],
  "video_url": null,
  "has_voice": false,
  "document_url": null,
  "link_preview": {
    "url": "https://telegram.org/blog/april-2026",
    "site_name": "Telegram",
    "title": "April 2026 Update",
    "description": "100% private AI Editor, new polls, Live Photos...",
    "image_url": "https://telegram.org/..."
  }
}
```

#### Minimal

Keeps: `channel`, `post_id`, `url`, `date`, `text`, `views`, `forwarded_from`, `photos`, `video_url`, `link_preview`.

#### RAG (Markdown)

Each dataset row contains `channel`, `post_id`, `url`, `date`, `views`, and a `markdown` block with the available text and link preview. The Console Overview table displays this block in **Markdown (RAG)**; JSON and CSV exports retain the `markdown` field.

When Telegram exposes only Open Graph metadata for a direct `postUrls` request, all three formats include `og_source: "metadata_only"`. This preview can be incomplete; unavailable dates and view counts are not measured values. RAG Markdown labels the preview and shows **views unavailable** instead of inventing zero. A real zero remains `0 views`.

```markdown
## t.me/durov/478

**Channel:** durov · 2026-04-23T17:12:59+00:00 · 1500000 views

🔄 Your massive monthly Telegram update is out.
🧠 100% private AI Editor for your outgoing messages.
🗳 The most powerful polls in any messenger...

**Link preview:** [April 2026 Update](https://telegram.org/blog/april-2026)
```

### Output fields

| Field            | Type              | Description                                                                             |
| ---------------- | ----------------- | --------------------------------------------------------------------------------------- |
| `channel`        | string            | Handle without `@`, e.g. `durov`.                                                       |
| `post_id`        | integer           | Numeric ID within channel (used for pagination).                                        |
| `url`            | string            | Canonical `t.me/<channel>/<id>`.                                                        |
| `date`           | string (ISO-8601) | When posted (UTC).                                                                      |
| `text`           | string|null      | Post body. `<br>` converted to `\n`, all formatting stripped.                           |
| `views`          | integer|null     | Parsed from abbreviated `1.5K`/`2M` notation; precision is limited by Telegram’s public display. Unavailable values are null or absent in metadata-only full output.                                   |
| `forwarded_from` | string|null      | Original author if this is a forward.                                                   |
| `reply_to_url`   | string|null      | URL of post being replied to, if any.                                                   |
| `photos`         | array\[string]|null | CDN URLs for photo attachments; null or absent when metadata-only output cannot determine attachments.                                                         |
| `video_url`      | string|null      | Direct video URL (may be time-limited CDN link).                                        |
| `has_voice`      | boolean           | True if post contains a voice message.                                                  |
| `document_url`   | string|null      | Direct document URL for file uploads.                                                   |
| `link_preview`   | object|null      | `{url, site_name, title, description, image_url}` if post includes a link preview card. |
| `matched_keyword` | string (optional) | The input keyword whose channel search returned this post. Present only when `keywords` is used, in every output format. |
| `og_source`      | string (optional) | `metadata_only` for a limited Open Graph preview, in every output format. Omitted when widget data is available. |
| `markdown`       | string (RAG only) | Markdown block with available content, metadata and any preview limitation. |

### Integrations

#### Python — quick RAG ingest

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("tugelbay/telegram-posts-scraper").call(run_input={
    "channels": ["durov"],
    "outputFormat": "rag",
    "maxItems": 100
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    vector_db.ingest(item["markdown"])
```

#### JavaScript/Node

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

const client = new ApifyClient({ token: "<YOUR_APIFY_TOKEN>" });
const run = await client.actor("tugelbay/telegram-posts-scraper").call({
  channels: ["binance_announcements"],
  sinceIso: "2026-04-01T00:00:00Z",
  minViews: 10000,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

#### MCP (Claude / GPT agents)

Expose this actor as a tool via the Apify MCP Server (`mcp.apify.com`). Your agent gets a Telegram-read tool with no credentials to manage:

```json
{
  "name": "scrape_telegram_channel",
  "apifyActor": "tugelbay/telegram-posts-scraper",
  "description": "Read recent public Telegram posts from one or more channels"
}
```

#### LangChain / LlamaIndex

```python
from langchain_apify import ApifyWrapper
from langchain_core.documents import Document

## set APIFY_API_TOKEN in your environment
apify = ApifyWrapper()
loader = apify.call_actor(
    actor_id="tugelbay/telegram-posts-scraper",
    run_input={"channels": ["durov"], "outputFormat": "rag", "maxItems": 100},
    dataset_mapping_function=lambda item: Document(
        page_content=item.get("markdown") or "",  # "rag" outputFormat emits the body as `markdown`
        metadata={"source": item.get("url"), "channel": item.get("channel"),
                  "views": item.get("views"), "date": item.get("date")},
    ),
)
docs = loader.load()  # list[Document], ready for chunking + embedding
```

#### n8n

Use the community node `@apify/n8n-nodes-apify` → **Run Actor** operation:

- Actor: `tugelbay/telegram-posts-scraper`
- Input JSON: `{"channels": ["{{ $json.channel }}"], "maxItems": 20, "outputFormat": "full"}`
- Output dataset items are available as `$json` in downstream nodes.

#### Make

Use the Apify app (built-in) → **Run an Actor** module:

- Select actor: `tugelbay/telegram-posts-scraper`
- Input: map `channels`, `maxItems`, `outputFormat` fields
- Results are passed to next modules as JSON array.

#### Zapier

Use Apify action app → **Run Actor**:

- Actor: `tugelbay/telegram-posts-scraper`
- Input fields: populate `channels` (comma-separated), `maxItems`, `outputFormat`
- Output: dataset items feed into your Zap (email, Slack, Sheets, etc.).

### Use cases

- **Brand & competitor monitoring** — watch a competitor's announcement channel, alert on keywords.
- **Crypto signal tracking** — monitor shilling channels, extract mentioned token tickers via regex over `text`.
- **AI news feed** — aggregate AI/ML channels, dedupe, feed to LLM-summary pipelines.
- **Channel research for ads** — before sponsoring a channel, pull last 100 posts, measure view decay, engagement.
- **Internal KB from public team channel** — teams that use public Telegram for announcements can ingest their own channel into Notion/Confluence.
- **Local news aggregation** — hundreds of Telegram channels for every country/city — aggregate by location.
- **Dataset building** — label corpus for abuse detection, sentiment analysis, language identification.
- **Academic research** — social-network effects without needing Telegram developer registration.
- **Journalism leads** — monitor OSINT channels during breaking events.
- **Public figure tracking** — read every post from a politician/CEO/founder channel as it arrives.

### Cost estimation

| Scenario                                      | Posts   | Cost (FREE tier) | Cost (GOLD tier) |
| --------------------------------------------- | ------- | ---------------- | ---------------- |
| Daily 20-post feed from 5 channels            | 100/day | $0.20/day        | $0.12/day        |
| Monthly digest — 500 posts across 10 channels | 500     | $1.00            | $0.60            |
| One-off research dump — 10,000 posts          | 10,000  | $20.00           | $12.00           |
| RAG cold-start — 100 channels × 200 posts     | 20,000  | $40.00           | $24.00           |

Live PPE pricing verified on 2026-07-14 is **$0.002 per post on FREE** and **$0.0012 per post on GOLD and higher tiers**, plus the small Actor-start event.

Compare to running your own Telethon instance: $5–10/mo proxy + SIM card risk + ~40 hours maintenance/year.

> **Same features on all plans.** Free and paid accounts use the same fields, filtering and incremental logic. There is no Actor monthly quota. The shared `maxItems` ceiling, finite scan budget and run spending limit still apply. Scheduled state is isolated by caller identity and monitor key on both plans.

### FAQ

**Do I need a Telegram account?** No. No account, no phone, no 2FA. The actor reads public channel previews that Telegram serves to every indexing bot.

**What about private channels?** Private channels and channels with "web previews" disabled return empty. No way around that without a real account — which we explicitly don't do.

**Can it read comments?** Only when comments are public widgets on the channel post (rare). Private comment sections require account login.

**How fresh is the data?** Real-time. Each run hits Telegram live — you get the same posts a website visitor sees.

**What rate limits apply?** In validation, 40 rapid requests to `t.me/s/` returned 200. Apify proxy handles any edge-case throttling. Heavy runs (>10k posts) should use `RESIDENTIAL` proxy group for safety.

**Is this legal?** Channel previews are public by design (they're indexed by Google). Standard web-scraping compliance applies — respect channel owners' rights, honor takedown requests, don't re-publish copyrighted material.

**What languages?** All. Text is extracted as-is. Telegram channels exist in every major language; the actor returns whatever bytes Telegram serves.

**Why `t.me/s/` and not the Bot API?** Bot API requires the bot to be a channel admin. `t.me/s/` requires nothing. Different access model — we chose simplicity.

**Can I get channel subscriber count?** Not currently. `t.me/s/<handle>` doesn't expose it. Could add via `t.me/<handle>` in a future version if requested.

### Troubleshooting

| Issue                               | Cause                                       | Fix                                                                                                                                        |
| ----------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| 0 posts returned for a channel      | Channel is private or has previews disabled | Confirm `t.me/s/<channel>` in a browser — if it shows no posts, this channel isn't scrapeable publicly.                                    |
| Some posts missing link previews    | Telegram doesn't always generate them       | Nothing to fix — `link_preview` is null when Telegram doesn't emit the card.                                                               |
| Photo URLs expire after a few hours | Telegram CDN signs media URLs               | Download media during the run, don't rely on URLs later. Or hit the page again when needed.                                                |
| Pagination stops early              | Hit oldest post in preview window           | Telegram only shows ~100–200 posts in `/s/` view for most channels. For deeper history, pass lower `before` IDs or use a Telethon account. |
| Rate limit / slowdown               | Heavy burst from one IP                     | Use `RESIDENTIAL` proxy group or lower concurrency.                                                                                        |

### Validation evidence and Telegram terms (2026-07-14)

Validation on 2026-07-14 preserves the public-preview boundary:

- Telegram documents public [post widgets](https://core.telegram.org/widgets/post), which are based on public post URLs.
- Telegram's current [Terms of Service](https://telegram.org/tos) remain authoritative; channel rights, privacy, copyright, and applicable law also apply.
- Each result retains channel, post ID, source URL, and UTC `date` so downstream users can verify the public source and snapshot time.
- Strict Actor QA validates schemas, metadata, links, Docker configuration, and PPE declarations before release.

This evidence does not grant access to private channels, recover deleted posts, verify authorship, or guarantee rankings, reach, or AI citations.

### Limitations

- No private channels, no group chats — only channel public previews.
- No comments — Telegram renders those client-side.
- Media URLs are CDN-signed and expire; download during the run if you need persistence.
- No reactions count — not in the public preview HTML.
- Subscriber counts not yet extracted (future work).
- Some channels have previews limited to last 100 posts — deeper history requires account-based access (out of scope).

### Changelog

- **0.1.30 (2026-07-31)** — Added opt-in cursor-backed incremental mode for Apify Tasks and Schedules. First runs seed a private per-caller monitor; later runs deliver only unseen channel posts and fail closed when a 1,000-post cursor boundary cannot be proven.
- **0.1.24 (2026-07-02)** — Removed a dead `telegram-post` charge call left over from an earlier pricing draft; billing is unchanged (it already runs on the `apify-default-dataset-item` PPE event via `push_data`).
- **0.1** (2026-04-24) — Initial release. Full / minimal / RAG formats. Pagination via `?before=`. Filters by views + date. MCP-compatible PPE pricing.

### Support & Feedback

If this actor saves you time, please **[leave a review](https://apify.com/tugelbay/telegram-posts-scraper)** on its Store page — it takes under a minute and is the main trust signal that helps other users find a reliable tool.

Hit a bug or missing a feature? Open a ticket on the Actor's **Issues tab** — issues are typically answered within 24-48 hours and fixes ship fast.

### Related Actors

- [youtube-transcript](https://apify.com/tugelbay/youtube-transcript) — Turn public video captions into text for the same research workflow.
- [substack-rss-intelligence](https://apify.com/tugelbay/substack-rss-intelligence) — Collect newsletter posts from official public RSS feeds.

# Actor input Schema

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

Public Telegram channel handles, @handles, or channel URLs such as https://t.me/durov and https://t.me/s/durov. Repeated forms of the same channel are fetched once in input order. Use postUrls for links with a post ID and before for pagination; channel URLs must not contain query parameters or fragments. Each channel must be public and have previews enabled.

## `keywords` (type: `array`):

Optional. Return only posts that match these words or phrases, searched inside each channel listed above with Telegram's own public channel search. Each keyword is searched separately; a post that matches several keywords is delivered once. Matching follows Telegram: it ignores letter case and may match word forms. This does not search all of Telegram, only the channels you list. Up to 20 keywords, 100 characters each.

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

Full Telegram post URLs (e.g. https://t.me/durov/478). If set together with channels, both sources are scraped.

## `search` (type: `string`):

Exact handle lookup using t.me/s/<handle>, not a search for channels by topic. Returns newest matching posts from that public preview. Leave empty if using channels/postUrls.

## `maxItems` (type: `integer`):

Maximum delivered posts shared across all sources. Ordinary channels return newest matching posts first, in channel input order. Filtered scans inspect up to 5x the remaining limit, with a minimum of 100 and maximum of 10000 posts per channel.

## `before` (type: `integer`):

Begin pagination at posts OLDER than this ID. Leave empty for newest first. Useful for catching up a specific date range. Ignored when incremental mode is enabled.

## `outputFormat` (type: `string`):

'full' includes every Telegram field (media, link preview, forwards). 'minimal' keeps key fields. 'rag' outputs one Markdown block per post, ready for vector DB.

## `minViews` (type: `integer`):

Drop posts with fewer than N views. 0 = no filter. Useful for feed-scanning to keep only viral posts.

## `sinceIso` (type: `string`):

ISO-8601 timestamp (e.g. 2026-01-01T00:00:00Z). Posts older than this are skipped before the delivered-post limit. Timestamps without an offset are interpreted as UTC. Leave empty for all posts.

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

Telegram has very light rate limits on /s/ pages, but Apify proxy is recommended for large runs. The default uses any Apify group.

## `incremental` (type: `boolean`):

Opt in to private cursor tracking for channel sources. The first run returns the latest sample; later runs with the same monitor key return only unseen posts. With keywords, each channel and keyword pair keeps its own cursor. Use this in an Apify Task + Schedule. The before field is ignored while enabled.

## `monitorKey` (type: `string`):

Optional cursor namespace inside this automatically isolated Apify Task, for example daily-competitors. Keep it stable across runs; change it to start a fresh cursor.

## Actor input object example

```json
{
  "channels": [
    "durov",
    "telegram"
  ],
  "maxItems": 10,
  "outputFormat": "full",
  "minViews": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "incremental": false
}
```

# Actor output Schema

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

Dataset with posts: channel handle, post ID, date posted, view count, text content, media URLs, and link preview.

# 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"
    ],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("tugelbay/telegram-posts-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",
    ],
    "maxItems": 10,
}

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

```

## MCP server setup

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