# Telegram Channel Scraper — New, Edited & Deleted Posts (`kaz_kakyo/telegram-channel-scrapper`) Actor

Scrape public Telegram channels without an account and get only the changes: new posts, edited posts, deleted posts. Keyless. JSON output with text, media URLs and timestamps for feeds, alerts and OSINT pipelines.

- **URL**: https://apify.com/kaz\_kakyo/telegram-channel-scrapper.md
- **Developed by:** [Heim AI](https://apify.com/kaz_kakyo) (community)
- **Categories:** Social media, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 post events

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 — New, Edited & Deleted Posts

Scrape public Telegram channels and get **only the changes**: new posts, edited posts, and deleted posts. Keyless — reads `https://t.me/s/<channel>` public preview HTML. No Telegram account, no user-level PII (no members, commenters, phones).

Built for feeds, alerts, OSINT pipelines, and agent workflows that need a stable delta stream instead of re-scraping full timelines.

### What it does

Each run polls the channels you list, diffs them against a persistent snapshot keyed by `monitorId`, and emits:

- **new** — post id appeared since last poll (or all visible posts on first seen, if `emitOnFirstSeen` is true)
- **edited** — same id, content hash changed (text / media type / media count; view ticks and CDN URL churn ignored)
- **deleted** — id was inside the visible window and has been missing for **two consecutive polls that could observe its position** (a poll whose window does not reach that far neither confirms nor counts; a transient Telegram page glitch never produces a false deletion; the row carries last-known text). The poll row's `pendingDeletionCount` shows first-miss candidates awaiting confirmation.

Plus one **poll** summary row per successfully checked channel.

### Quick start (Console)

1. Open the Actor → **Input**
2. Keep the prefill (`telegram`, `durov`) or paste your public handles
3. Set a stable `monitorId` (e.g. `prod-alerts`)
4. Run → first run emits baseline `new` posts (unless `emitOnFirstSeen: false`)
5. Schedule every 15 minutes with the **same** `monitorId` to receive only real deltas

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `channels` | string\[] | — (required) | Handles: `telegram`, `@durov`, `t.me/x`, `https://t.me/s/x`. Max 50/run. |
| `monitorId` | string | `default` | Snapshot namespace. `^[A-Za-z0-9_-]{1,64}$` |
| `scanDepth` | integer | `20` | Posts fetched per channel (20–100). Deletion detection covers this window only. |
| `emitOnFirstSeen` | boolean | `true` | First poll: emit all visible as `new`, or silently baseline. |

### Output row types

Every row includes `type`, `monitorId`, and `detectedAt` (ISO).

#### `type: "post"` (charged `post-event`)

```json
{
  "type": "post",
  "event": "new",
  "monitorId": "prod-alerts",
  "channel": "telegram",
  "postId": 28421,
  "postUrl": "https://t.me/telegram/28421",
  "text": "Hello from Telegram",
  "mediaType": "photo",
  "mediaUrls": ["https://cdn4.telegram-cdn.org/..."],
  "views": 1200000,
  "viewsRaw": "1.2M",
  "postedAt": "2026-07-30T12:00:00+00:00",
  "firstSeen": true,
  "detectedAt": "2026-08-01T05:00:00.000Z"
}
```

Edited (adds `previousText`):

```json
{
  "type": "post",
  "event": "edited",
  "monitorId": "prod-alerts",
  "channel": "telegram",
  "postId": 28421,
  "postUrl": "https://t.me/telegram/28421",
  "text": "Updated caption",
  "previousText": "Hello from Telegram",
  "mediaType": "photo",
  "mediaUrls": ["https://cdn4.telegram-cdn.org/..."],
  "views": 1200000,
  "viewsRaw": "1.2M",
  "postedAt": "2026-07-30T12:00:00+00:00",
  "firstSeen": false,
  "detectedAt": "2026-08-01T05:15:00.000Z"
}
```

Deleted (last-known content from snapshot):

```json
{
  "type": "post",
  "event": "deleted",
  "monitorId": "prod-alerts",
  "channel": "telegram",
  "postId": 28410,
  "postUrl": "https://t.me/telegram/28410",
  "text": "fake for delete test",
  "mediaType": null,
  "mediaUrls": [],
  "views": null,
  "viewsRaw": null,
  "postedAt": "2026-07-29T09:00:00+00:00",
  "firstSeen": false,
  "detectedAt": "2026-08-01T05:15:00.000Z"
}
```

#### `type: "poll"` (charged `channel-poll`)

```json
{
  "type": "poll",
  "monitorId": "prod-alerts",
  "channel": "telegram",
  "status": "ok",
  "postsVisible": 20,
  "windowOldestPostId": 28401,
  "windowNewestPostId": 28421,
  "newCount": 1,
  "editedCount": 0,
  "deletedCount": 0,
  "pendingDeletionCount": 0,
  "firstRun": false,
  "polledAt": "2026-08-01T05:15:00.000Z",
  "detectedAt": "2026-08-01T05:15:00.000Z"
}
```

#### `type: "error"` (uncharged; run still succeeds)

```json
{
  "type": "error",
  "monitorId": "prod-alerts",
  "channel": "https://t.me/+inviteHash",
  "error": "Invalid channel input \"https://t.me/+inviteHash\" — use a public handle, @handle, or t.me link (not invite links).",
  "errorCode": "invalid_channel",
  "detectedAt": "2026-08-01T05:00:00.000Z"
}
```

`errorCode` values: `invalid_channel`, `channel_not_found`, `channel_preview_disabled`, `fetch_failed`, `skipped_channel_limit`, `skipped_charge_limit`, `invalid_monitor_id`, `no_channels`, `input_overflow` (more than 200 raw entries), `monitor_busy` (another live run holds this `monitorId` — see below), `state_corrupt` (snapshot state failed validation — polling stops, state untouched).

**Concurrency guard (`monitor_busy`):** each `monitorId` is protected by a server-side lock (Apify request-queue lock API — atomic and ownership-enforced, so exactly one of two overlapping runs can hold it, including takeover of a dead run's expired lock). The losing run records one uncharged `monitor_busy` error row and exits successfully — schedule ticks that overlap a still-running poll are skipped safely instead of double-billing the same delta. The lock is renewed with ownership verification during long runs and expires automatically (8 min) if a run dies; a run that can no longer verify ownership stops all new charges immediately. Each `monitorId` keeps one small named request queue (`tcd-lock-…`) as its lock.

### Scheduling recipe

Use Apify **Schedules** with a fixed `monitorId`:

- Cron: `*/15 * * * *` (every 15 minutes)
- Input: same `channels` + same `monitorId` every time
- Idle cost ≈ start fee + one `channel-poll` per channel per tick; you only pay `post-event` when something actually changes

### API curl recipe

```bash
curl "https://api.apify.com/v2/acts/kaz_kakyo~telegram-channel-delta/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channels": ["telegram", "durov"],
    "monitorId": "prod-alerts",
    "scanDepth": 20,
    "emitOnFirstSeen": true
  }'
```

Then read the run's default dataset for post/poll/error rows.

### MCP / agents

Call this Actor from Apify MCP or any agent that can start an Actor run and read a dataset. Keep `monitorId` stable across scheduled calls so the agent receives a clean delta stream (not full timelines). Error rows are uncharged and the run succeeds on bad input — safe for autonomous retries.

### Honest limitations

- **Deletion window** — deletions are detectable only while the post id is still inside the visible `scanDepth` window (~last N posts). Older history is truncated, not reported as deleted.
- **Deletion latency** — a deletion is confirmed on the second consecutive poll where the post is missing (one extra poll interval of latency, by design, to avoid false positives).
- **Billing exactness** — billing is exactly-once in normal operation, including Apify platform migrations (each billed row settles into the snapshot immediately). In the worst case of a hard crash between a charge and the next state write, the affected channel's current delta can be re-emitted (and re-billed) once on the next run.
- **Media-only edits** — hash uses text + mediaType + media URL *count*. Replacing an image without changing type/count/text may be missed.
- **Markup drift** — t.me public preview HTML can change; selectors may need updates.
- **Public preview only** — private channels and channels with preview disabled cannot be polled (you get `channel_preview_disabled` / `channel_not_found`).
- **No user-level data** — by design: channel posts only, never members/commenters/profiles/phones.

### Pricing

| Event | Price |
|---|---|
| Actor start (`apify-actor-start`) | $0.005 |
| Channel poll (`channel-poll`) | $0.001 |
| Post event (`post-event`) | $0.001 |

**Worked example:** 5 channels every 15 minutes = 96 runs × ($0.005 start + 5 × $0.001 polls) ≈ **$0.96/day idle**, plus **$0.001 per actual post event**. Hourly polling of the same 5 channels ≈ $0.24/day.

### FAQ

**Why poll rows?**\
They prove a channel was checked even when nothing changed, and carry window bounds + counts for monitoring/health dashboards.

**What happens on the first run?**\
With `emitOnFirstSeen: true` (default), every visible post is emitted as `new` (`firstSeen: true`) and a snapshot is saved. With `false`, only the poll row is emitted and the snapshot is baselined silently.

**How does `monitorId` work?**\
Snapshots live in named KV store `telegram-channel-delta-state` under key `monitor-<monitorId>`. Different monitorIds never share state — use separate ids for separate jobs/customers.

**Is this against Telegram ToS?**\
This Actor only fetches public `t.me/s/` preview pages (the same HTML any browser can load). No account, no login, no private API, no member scraping.

# Actor input Schema

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

Public Telegram channel handles to monitor. Accepts handle, @handle, t.me/handle, or t.me/s/handle. Invite links (+… / joinchat) are rejected. Max 50 per run. Prefill channels are public and keyless.

## `monitorId` (type: `string`):

Snapshot namespace for this watch job. Same monitorId + channels → only real deltas on later runs. Must match ^\[A-Za-z0-9\_-]{1,64}$ (an invalid value records one uncharged error row and the run exits successfully — never silently merged).

## `scanDepth` (type: `integer`):

How many recent posts to fetch per channel (20 per t.me/s page via ?before= pagination). Deletion detection only covers posts still inside this window.

## `emitOnFirstSeen` (type: `boolean`):

When true (default), the first poll of a channel under this monitorId emits every visible post as event:new (charged). When false, the first poll silently baselines and only later deltas are emitted.

## Actor input object example

```json
{
  "channels": [
    "telegram",
    "durov"
  ],
  "monitorId": "default",
  "scanDepth": 20,
  "emitOnFirstSeen": true
}
```

# Actor output Schema

## `events` (type: `string`):

New, edited and deleted posts detected since the previous run, plus one poll row per 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": [
        "telegram",
        "durov"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kaz_kakyo/telegram-channel-scrapper").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": [
        "telegram",
        "durov",
    ] }

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

```

## MCP server setup

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

```

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/NOHjxCTBZjWH5piK5/builds/WPhpCQf1evQegmcsA/openapi.json
