# Telegram Channel Enrichment | $2.40/1K | Reach & Velocity (`feedminer/telegram-channel-enrichment`) Actor

Enrich Telegram channels from a list of @handles: exact subscriber count, posting cadence, activity recency, median post reach, reach-to-size ratio, script and content mix. Dead or non-channel handles come back as settled negatives. Flat $2.40 per 1,000 enriched rows. By FeedMiner.

- **URL**: https://apify.com/feedminer/telegram-channel-enrichment.md
- **Developed by:** [FeedMiner](https://apify.com/feedminer) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 channel enricheds

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

<div style="border-left:3px solid #1C1917;background:#FAFAF9;padding:15px 20px;border-radius:3px">
<strong>A list of Telegram handles in, one enriched row per channel out.</strong> The exact subscriber count, how recently and how often it posts, its median reach, reach per subscriber, the script it writes in and the content it leans on — plus, on demand, whether its reach is growing or shrinking. Delivered as JSON, CSV or Excel, or read straight from the API. No Telegram account of yours is involved, and nothing has to be connected.
</div>

🔓 **Nothing to connect.** No Telegram login, no phone number, no bot token, no API key, no cookies to paste. Every read is a public `t.me` page Telegram serves to anyone, so there is nothing in the loop that can expire on you.

📊 **The columns nobody else sells.** The Telegram scrapers on the Store hand you the fields Telegram already prints — subscribers, bio, verification, media counts. This one computes the cadence, the median reach, the reach-per-subscriber ratio, the content mix, and the settled reach velocity: whether a channel is reaching further this month than last.

⚡ **63 handles in 10 seconds.** Measured on platform run `fSl5eXaKscUcw05id`, at the 256 MB this Actor asks for. With the reach velocity on, a 32-handle run took 2 minutes and walked 807 pages of history across its 31 channels.

💸 **Two charged events, and a market that prices a channel between $1 and $30 per 1 000.** You pay per delivered row, and separately for the reach-velocity column only on the rows that actually carry one. A handle Telegram would not serve is delivered and charged nothing at all.

![How Telegram Channel Enrichment works: a list of handles in, one enriched row per channel out, with the reach velocity on demand](https://api.apify.com/v2/key-value-stores/RPT1LNaNbQoVImUE9/records/telegram-channel-enrichment-how-it-works-v2.png)

#### Copy to your AI assistant

```
feedminer/telegram-channel-enrichment on Apify. Enriches a list of Telegram handles anonymously — no account, no phone number, no bot token, no cookies. For each handle it reads the public join page t.me/<handle>, and for the ones that turn out to be channels the message preview t.me/s/<handle>; with reachVelocity on it pages that preview backwards to compare settled reach. One dataset row per handle, whatever the handle turned out to be (handle, shape, url, title, description, verified, stated, subscribers, members, online, monthly_users, photos, videos, links, last_post_at, last_post_days, posts_per_week, median_views, views_per_subscriber, dominant_script, dominant_content, sample_posts, reach_velocity, velocity_status, velocity_settled_posts, velocity_cohort_gap_days, velocity_history_days, velocity_pages, velocity_capped, unreadable_cause). Four measured limits to respect: shape tells you what the handle really is — channel, channel_no_preview, group, bot, user, unknown, missing or unreadable — and only "channel" carries the derived columns; reach_velocity is null unless velocity_status is "computed", and on a 32-row run only 13 rows computed one; there is no subscriber growth anywhere, what moves is reach; median_views inherits Telegram's own abbreviation of view counts, so it is a band and not an exact figure. Call ApifyClient("TOKEN").actor("feedminer/telegram-channel-enrichment").call(run_input={"handles": ["bbcrussian", "tproger", "bloomberg"], "reachVelocity": True}), then client.dataset(run["defaultDatasetId"]).list_items().items. Required: handles (string[], at least one, with or without @, as a t.me link, any case, duplicates fold). Optional: reachVelocity (bool, default false, charged separately), velocitySpanDays (integer 20-180, default 40), debugMode (bool, default false). Full spec: GET https://api.apify.com/v2/acts/feedminer~telegram-channel-enrichment/builds/default (Bearer TOKEN) → inputSchema, actorDefinition.storages.dataset, readme. Token: https://console.apify.com/account/integrations
```

***

### How to enrich Telegram channels

Paste the handles you already have. That is the whole input.

##### The smallest run

```json
{ "handles": ["bbcrussian", "tproger", "bloomberg"] }
```

##### However the handles were sent to you

```json
{ "handles": ["@bbcrussian", "https://t.me/tproger", "t.me/s/durov", "Bloomberg"] }
```

With or without the `@`, as a `t.me` link, as a pasted `t.me/s/` preview URL, in any case. Duplicates fold, so a column copied out of a spreadsheet runs unedited.

##### With the reach velocity

```json
{ "handles": ["bbcrussian", "meduzalive", "varlamov"], "reachVelocity": true }
```

This is the expensive tier and the reason to buy the Actor. It pages each channel's history backwards until it holds enough posts old enough to compare — on the run behind this page that was 26.2 requests per row against 1.84 without it.

##### A quiet list, over a wider window

```json
{
  "handles": ["bloomberg", "spacex", "microsoft", "netflix"],
  "reachVelocity": true,
  "velocitySpanDays": 180
}
```

A channel that publishes twice a month has no six-week reach trend, and at the default 40 days it answers `too_few_settled_posts` rather than inventing one. Widening the window is exactly what the setting is for.

***

### Input parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `handles` | array | required | The Telegram usernames to enrich, at least one. With or without `@`, as a `t.me` or `t.me/s/` link, in any case; case is folded and duplicates fold on read. A string with no usable username in it is dropped and logged rather than sent to Telegram |
| `reachVelocity` | boolean | `false` | Turns on the reach-velocity column. **Charged separately, and only on the rows that actually carry it.** Leave it off and the second event is never touched |
| `velocitySpanDays` | integer | `40` | How far back the reach comparison looks, 20 to 180. Only posts older than ten days count towards it, which is why the floor is not lower. Widen it for quiet channels |
| `debugMode` | boolean | `false` | Verbose log lines for support. Leave off |

`reachVelocity` is what decides both the run's clock and its bill; `velocitySpanDays` decides how far the walk goes before it stops, and therefore how many of your channels can answer at all.

***

### What data can you extract from Telegram?

One row per handle — **every handle gets a row**, including the ones that turn out not to be channels. (A string this Actor cannot read a username out of at all is dropped before Telegram is asked, and logged; it is the one input that produces no row.) That is how you reconcile a list somebody handed you.

**What the handle turned out to be**

| Field | Type | Description |
|---|---|---|
| `handle` | string | The username as normalised: lowercased, `@` and any `t.me` wrapping stripped |
| `shape` | string | `channel`, `channel_no_preview`, `group`, `bot`, `user`, `unknown`, `missing` or `unreadable` — see the table below |
| `url` | string | The handle's page on `t.me` |
| `stated` | string | The join page's own sentence about the audience, verbatim — `"387 049 subscribers"`, `"95 842 members, 2 047 online"` |
| `unreadable_cause` | string | Present only on an `unreadable` row: `join_page_refused` or `preview_refused` |

**What Telegram states about a channel**

| Field | Type | Description |
|---|---|---|
| `title` | string | The channel's display name. Empty when no preview served |
| `description` | string | The channel's about text |
| `verified` | boolean | Telegram's own blue check |
| `subscribers` | integer | A channel's exact count, as the join page states it in full. `0` on every other shape |
| `members` | integer | A group's exact count. `0` on every other shape |
| `online` | integer | A group's live presence at the moment of the read |
| `monthly_users` | integer | A bot's audience. `0` on every other shape |
| `photos`, `videos`, `links` | integer | The channel's lifetime counts. **Rounded**: the preview prints them abbreviated, so `21300` is a `21.3K` read back |

**What this run derives from the messages**

| Field | Type | Description |
|---|---|---|
| `last_post_at` | string | When the newest sampled message was published, RFC 3339. Empty when no message carried a date |
| `last_post_days` | number | How stale the channel is, in days. **Null** when no message carried a date |
| `posts_per_week` | number | Cadence over the sample's own span. **Null** when the sample spans no time — one message, or several sharing a timestamp |
| `median_views` | integer | The middle message's reach, as Telegram prints it — **rounded**, so it names a band and not an exact figure. The median and not the mean: one message carried by an outside share moves a mean by an order of magnitude and says nothing about the channel. **Null** when no message carried a view count |
| `views_per_subscriber` | number | `median_views ÷ subscribers`, as a fraction — `0.105` is 10.5 %. **Null** when either side is unknown |
| `dominant_script` | string | The writing system the sample uses: `latin`, `cyrillic`, `arabic`, `han`, `hebrew`, `greek`, or `unknown`. **A script reading, never a language one** |
| `dominant_content` | string | What the channel leans on: `text`, `photo`, `video`, `link`, `poll` and the other kinds Telegram labels |
| `sample_posts` | integer | How many messages the derived columns were computed from |

**The reach velocity, when you ask for it**

| Field | Type | Description |
|---|---|---|
| `reach_velocity` | number | The newer settled cohort's median reach over the older's. **Above 1 means reach is growing.** Null unless `velocity_status` is `computed` |
| `velocity_status` | string | Why the column is there, or why it is not — see the table below |
| `velocity_settled_posts` | integer | How many posts the ratio rests on: those past the ten-day settling window **and** inside the span you asked for |
| `velocity_cohort_gap_days` | number | How far apart the two cohorts' median ages are. A ratio across one day means far less than the same ratio across a month. **Null** when there were too few settled posts to split into two cohorts |
| `velocity_history_days` | number | The age of the oldest settled post the walk kept inside your window — bounded by the span you asked for, and shorter than it whenever the walk stopped first. **Null** when the walk collected nothing to compare |
| `velocity_pages` | integer | How many preview pages the walk spent on this row |
| `velocity_capped` | boolean | Whether the page cap is what stopped the walk, so the history is shorter than asked for |

![What one row of Telegram Channel Enrichment carries, field by field: shape, cadence, reach per subscriber and the reach velocity](https://api.apify.com/v2/key-value-stores/RPT1LNaNbQoVImUE9/records/telegram-channel-enrichment-row-anatomy-v1.png)

**A computed column is `null` when it could not be measured, never `0`** — a channel that posted once has no cadence, and a zero there would read as a measurement. The counts Telegram states are plain integers and use `0` for "this shape does not have one": a group ships `subscribers: 0`, a channel ships `members: 0`. One place is genuinely ambiguous, and it is named here rather than hidden: a subscriber count this Actor could not parse also ships `0`, and nothing distinguishes it from a real zero.

##### Example row

```json
{
  "handle": "bbcrussian",
  "shape": "channel",
  "url": "https://t.me/bbcrussian",
  "title": "BBC News | Русская служба",
  "verified": true,
  "stated": "387 049 subscribers",
  "subscribers": 387049,
  "members": 0,
  "online": 0,
  "monthly_users": 0,
  "photos": 21300,
  "videos": 4150,
  "links": 61300,
  "last_post_at": "2026-09-20T03:42:26Z",
  "last_post_days": 0.03616003834597222,
  "posts_per_week": 114.76853932584268,
  "median_views": 40700,
  "views_per_subscriber": 0.10515464450237567,
  "dominant_script": "cyrillic",
  "dominant_content": "link",
  "sample_posts": 20,
  "reach_velocity": 0.9664179104477612,
  "velocity_status": "computed",
  "velocity_settled_posts": 924,
  "velocity_cohort_gap_days": 14.967361111111106,
  "velocity_history_days": 39.990477167975605,
  "velocity_pages": 64,
  "velocity_capped": false
}
```

Verbatim from platform run `1EZIcKTcaHGn3qUV8`, minus the `description` field to keep it readable. That row cost 64 preview pages: 924 posts old enough to compare, reaching 40.0 days back, for a channel publishing 114.8 times a week.

#### What `shape` says, and which columns it fills

| `shape` | What it means | Which columns are filled |
|---|---|---|
| `channel` | a channel whose message preview served | all of them |
| `channel_no_preview` | its join page states a subscriber count, its preview will not serve | the stated facts only |
| `group` | a group | `members`, `online` |
| `bot` | a bot | `monthly_users` |
| `user` | a person's account | the handle, and that it is held |
| `unknown` | its join page states an audience in a wording this Actor does not recognise | `stated`, Telegram's own sentence, verbatim |
| `missing` | nobody holds this handle — asked twice, from two reads | nothing, which is the answer |
| `unreadable` | Telegram would not serve this handle, twice, from two addresses | `unreadable_cause`. **Never charged** |

#### Why `velocity_status` matters as much as the number

| `velocity_status` | What happened |
|---|---|
| `computed` | `reach_velocity` is there, and rests on the posts and the gap the same row reports |
| `not_requested` | you left `reachVelocity` off |
| `not_applicable` | the handle is not a channel with a readable preview |
| `too_few_settled_posts` | fewer than six posts inside your window are older than ten days. Widen `velocitySpanDays` |
| `cohorts_too_close` | the posts are there but bunched: comparing them would measure one afternoon |
| `views_unavailable` | the settled posts carry no view counts, so there is nothing to compare |

A quiet channel will often say `too_few_settled_posts` at the default 40 days — fifteen of the 31 channels on run `1EZIcKTcaHGn3qUV8` did. That is the honest answer, not a failure: a channel that published three times in six weeks has no six-week reach trend. The other refusal is narrower — `bloomberg`, at 0.6 posts a week, had six settled posts inside the window but bunched 0.22 days apart, so its row says `cohorts_too_close` rather than reporting one afternoon as a six-week trend. Widening `velocitySpanDays` is what both refusals are asking for.

***

### What this Actor covers

![What Telegram Channel Enrichment reaches, and what it does not](https://api.apify.com/v2/key-value-stores/RPT1LNaNbQoVImUE9/records/telegram-channel-enrichment-coverage-v2.png)

This Actor enriches handles you already have. **Finding channels you do not know about is out of scope**, and so is pulling message text, media or comments in bulk — no FeedMiner Actor sells either today, so nothing on this page will point you at a sibling that does not exist. One run is one snapshot with nothing carried over: schedule the Actor again to compare two moments.

***

### What it does not promise

Named here because a competitor's page might promise them and this one cannot.

**No subscriber growth.** Telegram states a channel's subscriber count and never its history, and every third-party site holding one is behind a wall — a Cloudflare block on four of five surveyed, a sign-in on the fifth. What grows or shrinks here is **reach**, derived inside a single run from post history. There is no membership delta, and anything selling you one from these surfaces is guessing.

**No reactions.** Telegram's web preview carries none. Engagement here is views, and the ratio is named `views_per_subscriber` for exactly that reason.

**Script, not language.** `dominant_script` says `cyrillic`, not `russian`. Cyrillic does not separate Russian from Ukrainian, and the column is named for what it measures.

***

### Limits, said plainly

These are measured facts about what an anonymous read of Telegram can see, not product decisions, and they are on this page so you learn them before you pay rather than after.

- **The velocity walk is capped at 120 pages per channel.** 120 pages hold 2 400 messages, so a channel posting more than about 60 times a day reaches the cap before it reaches a 40-day window; `velocity_capped` says so and `velocity_history_days` says how far it actually got. Three of the 31 channels hit it on run `1EZIcKTcaHGn3qUV8` — `tass_agency` at 1 730 posts a week, `rbc_news` at 221 and `kommersant` at 199. Two of the three still delivered a velocity, over 38.3 and 38.7 days. `tass_agency` did not: 120 pages of it reach back only 14.8 days, so its cohorts landed 2.3 days apart and the row says `cohorts_too_close` rather than selling an afternoon's scatter as a trend.
- **Half a list of "Telegram channels" may not be channels.** On run `fSl5eXaKscUcw05id`, 63 handles assembled to look like a marketing spreadsheet came back as 32 channels, 20 held by nobody, 5 people, 3 groups, 2 bots, and 1 channel that states a size and serves no preview. Five of those twenty dead handles were invented strings, planted to exercise the branch; drop them and it is still 26 non-channels out of 58. Every one of them got a row either way. That run also predates the confirming second read, so its `missing` verdicts were single reads — which is precisely why the build that ships today asks twice.
- **The subscriber count is exact; almost nothing else is.** Telegram states the audience in full on the join page — `bbcrussian` read 387 049 — and rounds everything the preview prints: the view counts, and the lifetime `photos`, `videos` and `links` totals with them. A `40.7K` is anywhere inside a band, and `median_views`, `views_per_subscriber` and those three totals all inherit it. `subscribers`, `members`, `online` and `monthly_users` come off the join page and are exact.
- **Telegram's own pages are occasionally thin, and this Actor asks twice before calling a handle dead.** `lentadnya` — a live 332 425-subscriber channel — answered with a page stating nothing on three of four runs minutes apart. Nothing in such an answer distinguishes it from a handle nobody holds, so every `missing` verdict is confirmed by a second read before it is delivered. A dead handle therefore costs two requests rather than one.
- **A handle can read `missing` on one run and `channel` on the next.** The cause that was measured is the thin page above: Telegram occasionally answers with a document stating nothing. There is a second candidate that was **not** probed — Telegram hides some channels from some countries and this Actor does not pin its exit country — so if a handle keeps flapping, run it again before concluding anything about which of the two you met.
- **Reach velocity is refused more often than it is answered, by design.** On run `1EZIcKTcaHGn3qUV8`, 13 of the 32 rows carried a ratio. The other 19 were quiet channels, bunched bursts, or handles that were not channels at all — and every one of them says which.

***

### Pricing

Two charged events, because the two tiers are genuinely different products:

| Event | What fires it |
|---|---|
| `row` | one delivered row that carries an answer, whatever the handle turned out to be |
| `velocity` | a supplement, **only** on the rows that actually carry a reach velocity |

What each event means is fixed, and it is what this section is for:

- A handle Telegram would not serve is delivered so your list reconciles, and **charged nothing at all**.
- A handle nobody holds *is* charged: "this account does not exist" is what lets you purge your list, and establishing it costs two reads.
- A velocity walk that spent its requests and came back `too_few_settled_posts` or `cohorts_too_close` bills **no** supplement. You pay for the column only when the row carries a ratio — on run `1EZIcKTcaHGn3qUV8` that was 13 rows of 32.
- Leave `reachVelocity` off and the second event is never touched.
- A run stopped mid-walk keeps what it collected: run `VpKMFnwv2us3aHQnY` was killed 62 seconds into a velocity walk, delivered the 23 rows it already had, and fired both charges on its way out — 23 rows and 6 velocities. The supplement is charged by the exit path, not at the end of a loop that never finished.

##### Against the market

From a sweep of the Apify Store taken on 20 September 2026: twenty-two Telegram Actors, roughly 1 320 monthly users between them, and no leader. The ones charging **per channel** rather than per message:

| Actor | Users / 30 days | Charges |
|---|---|---|
| `viralanalyzer/telegram-channel-scraper` | 28 | $0.03 per post or channel summary |
| `zhorex/telegram-channel-scraper` | 6 | $0.02 per channel through the no-auth web preview, $0.05 through a Telegram session |
| `agentx/telegram-info-scraper` | 36 | $0.0052 per result |
| `truefetch/telegram-profile` | 67 | $0.003 per result |
| `automation-lab/telegram-scraper` | 87 | $0.002 per channel-info |
| `khadinakbar/telegram-channel-scraper` | 86 | $0.001 per channel-info |

That is **$1 to $30 per 1 000 channels** for a row of the fields Telegram already prints. Those are that sweep's prices and user counts, not necessarily today's.

**Nobody in that field sells the derived columns.** The cadence, the reach-over-size ratio, the content mix and above all the settled reach velocity are what this Actor is for, and the velocity is the one a competitor cannot copy by reading a single page.

##### Free tier

Apify's free plan includes $5 of platform usage a month and asks for no card — enough to run this Actor over a real list, velocity included, before you commit to anything.

***

### Advanced usage

##### Reconcile a list somebody handed you

Run it with `reachVelocity` off, then sort the dataset by `shape`. Every dead handle, every group filed as a channel, every bot and every personal account comes back named, at one request each — two for a dead handle, which is confirmed before it is delivered. On the 63-handle run that was half the list. The cheap tier exists for exactly this pass, and it is the one to run first on any list you did not build yourself.

##### Rank a shortlist by reach rather than by size

`subscribers` ranks channels by how many people once clicked join. `views_per_subscriber` ranks them by how many still read. On run `1EZIcKTcaHGn3qUV8` the two orders disagreed sharply: `varlamov`, at 236 869 subscribers, was read 1.19× its own size, while `tass_agency`, at more than twice that, was read 0.017× its own. Both had published within the day.

**Read the ratio next to `last_post_days`, always.** A dormant channel accumulates views on a handful of old posts against a subscriber count that has stopped moving, and it climbs this ranking without anybody reading it: the top of that run's ratio table is `twitter` at 55×, on three posts and 783 days of silence. Sorting by `views_per_subscriber` alone buys placements on dead channels.

##### Find the channels whose reach is actually growing

Turn `reachVelocity` on and filter `velocity_status == "computed"`, then sort by `reach_velocity` descending. Above 1 is growing. Do not treat the refusals as zeros — a `too_few_settled_posts` row is a channel too quiet to measure over your window, not a channel in decline, and filtering on the status is what keeps the two apart.

##### Two runs, one month apart

Nothing is kept between runs, so two runs a month apart compare cleanly: the same handles, the same window, and `subscribers`, `median_views` and `posts_per_week` on either side of whatever changed. This is also the only way to get a subscriber delta out of Telegram — by measuring the count twice yourself, not by buying a history nobody publishes.

***

### Integrate Telegram data into your stack

Results flow through the Apify API into Python, JavaScript, Google Sheets, Make, Zapier, n8n, Airtable or Slack. Runs can be scheduled and chained with other Actors.

##### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")

run = client.actor("feedminer/telegram-channel-enrichment").call(run_input={
    "handles": ["bbcrussian", "meduzalive", "tproger"],
    "reachVelocity": True,
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["velocity_status"] == "computed":
        print(row["handle"], row["subscribers"], round(row["reach_velocity"], 3))
    else:
        print(row["handle"], row["shape"], row["velocity_status"])
```

##### JavaScript

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

const client = new ApifyClient({ token: 'YOUR_API_TOKEN' });

const run = await client.actor('feedminer/telegram-channel-enrichment').call({
    handles: ['bbcrussian', 'meduzalive', 'tproger'],
    reachVelocity: true,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
const channels = items.filter((row) => row.shape === 'channel');
const growing = channels.filter((row) => row.reach_velocity > 1);
console.log(`${channels.length} channels, ${growing.length} growing`);
```

***

### Performance

Every figure below is from a platform run of 20 September 2026 on build 0.1.1, at the 256 MB this Actor asks for unless stated otherwise.

| Measure | Value | Measured on |
|---|---|---|
| Cheap tier, throughput | 63 handles in 10.1 s — 6.2 handles a second | Run `fSl5eXaKscUcw05id`, eight handles walked at once |
| Cheap tier, requests per row | 1.84 on a mixed list | Same run's funnel, recounted against the build that ships today — a dead handle now costs two reads, not one |
| Velocity tier, wall clock | 32 handles, 31 of them channels, in 121.0 s | Run `1EZIcKTcaHGn3qUV8`, `velocitySpanDays` 40 |
| Velocity tier, requests per row | 26.2 — 839 requests for 32 rows | Same run |
| History walked | 807 preview pages; three channels reached the 120-page cap | Same run; the deepest walk that stopped on its own was `vedomosti`, 40.0 days on 97 pages |
| Velocity rows delivered | 13 of the 32 rows | Same run's `velocity_status` column |
| Concurrency ceiling | Not found. 32 workers across four exits, ~3 400 requests, zero refusals | Four stress runs launched together |
| Rows kept on a kill | 23 of 23 already collected | Run `VpKMFnwv2us3aHQnY`, aborted 62 s into a walk |

Memory is the one setting worth leaving alone. The same 14 channels and the same walk took 40.4 s at 1024 MB, 45.8 s at 512 MB and 54.6 s at 256 MB — this Actor waits on the network far more than it computes, so quartering the memory costs about a third more wall clock and less than half the platform bill. **256 MB is the default for that reason**, and the 32-handle stress run walked 807 pages at it without trouble.

***

### FAQ

**Do I need a Telegram account, a phone number or a bot token?**
No. There is nothing to connect and nothing to keep alive on your side. Every read is a public `t.me` page — the join page, and the message preview above it — served to anyone who asks.

**How far back does it read?**
Without `reachVelocity`, one preview page: the last twenty or so messages, which is what the cadence, the median reach and the content mix are computed from. With it, the Actor pages backwards until it holds posts old enough to compare across the window you asked for, up to 120 pages per channel.

**Why did most of my channels come back without a reach velocity?**
Because a Telegram post keeps gathering views for about ten days, so only posts past that line can be compared. A channel that did not publish at least six of them inside your window answers `too_few_settled_posts`, and one whose posts are bunched into a couple of days answers `cohorts_too_close`. Both are refusals to make a number up. Widen `velocitySpanDays` — quiet corporate channels often need 120 or 180 days.

**A handle I know is alive came back `missing`.**
The cause that was measured is a thin page: Telegram occasionally serves a document stating nothing at all, and nothing in such an answer distinguishes it from a handle nobody holds — which is why a `missing` verdict is only delivered after a second, confirming read. There is a second candidate that was never probed: Telegram hides some channels from some countries, and this Actor does not pin its exit. Run the handle again. If it comes back as a channel, the first answer was wrong; which of the two produced it is not something this page will claim to know.

**What is the difference between `missing` and `unreadable`?**
`missing` is an answer — nobody holds this handle, established twice — and it is charged, because it is what lets you purge a list. `unreadable` is not an answer: Telegram would not serve the page at all. That row is still delivered so your list reconciles row for row, and it is never charged.

**Is the data live?**
Yes. Every run reads Telegram at that moment. Nothing is cached, nothing is resold, and nothing is kept between runs.

**What does it not do?**
It does not find channels for you, does not pull message text, media or comments in bulk, does not read reactions, does not list a group's members, and cannot see private or invite-only channels. It enriches handles you name, from what Telegram publishes to anyone.

***

### Support

- Found something off in the output, or need a field that is missing? Open the **Issues** tab on this Actor's page with the input you ran and what you expected. Issues are read daily.
- Need a custom variant — a different window, a different verdict, a column this one does not carry? Ask in the Issues tab; FeedMiner builds on request.

### Legal compliance

This Actor collects only public data, the same information any visitor can see on `t.me`. Channel descriptions and titles can contain personal data: processing information about people in the EU, the UK or California requires a lawful basis under GDPR or CCPA. Use the data for market research, analytics and business intelligence, not for spam or harassment.

***

**FeedMiner** · Fast, honestly priced scrapers, kept alive when platforms change.

Telegram Channel Enrichment · [X Tweet Scraper](https://apify.com/feedminer/x-tweet-scraper) · [All FeedMiner Actors](https://apify.com/feedminer)

# Actor input Schema

## `handles` (type: `array`):

The channel usernames you already have. With or without @, as a t.me link or a pasted t.me/s/ preview URL, in any case — duplicates fold. A handle that turns out to be a group, a bot, a person or nobody still gets a row saying so.

## `reachVelocity` (type: `boolean`):

Whether each channel's reach is growing or shrinking, measured inside this run. It is the expensive column: on a real mixed list a row costs under 2 requests without it and 26 on average with it, up to the 121 a channel posting hundreds of times a day spends reaching the 120-page cap. Charged separately, and only on the rows that actually carry it.

## `velocitySpanDays` (type: `integer`):

How far back the reach comparison looks. Only messages older than ten days count — a Telegram post keeps gathering views for about that long — so a channel too quiet to publish enough inside the window is reported as such rather than answered over a horizon you did not ask for. Widen it for quiet channels.

## `debugMode` (type: `boolean`):

Verbose log lines for support. Leave off.

## Actor input object example

```json
{
  "handles": [
    "bbcrussian",
    "tproger",
    "bloomberg"
  ],
  "reachVelocity": false,
  "velocitySpanDays": 40,
  "debugMode": false
}
```

# Actor output Schema

## `rows` (type: `string`):

No description

# 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 = {
    "handles": [
        "bbcrussian",
        "tproger",
        "bloomberg"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("feedminer/telegram-channel-enrichment").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 = { "handles": [
        "bbcrussian",
        "tproger",
        "bloomberg",
    ] }

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

```

## MCP server setup

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

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/dFAV585QvZ5y2kIMk/builds/zumIxijCbGmzLe488/openapi.json
