# Telegram Channel Analytics for Ad Buyers - Reach & Ad Share (`neverempty/telegram-channel-ad-analytics`) Actor

Vet public Telegram channels before buying an ad: exact subscribers, median views per post, reach % (ERR), posts per day, share of labelled ad posts, best hour and weekday, and ranks across the channels you list. Every row states the posts and dates it was measured on.

- **URL**: https://apify.com/neverempty/telegram-channel-ad-analytics.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Social media, Marketing, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $14.50 / 1,000 channel analyzeds

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

## Telegram Channel Analytics for Ad Buyers - reach, ad share and the **exact subscriber count**

**Check a Telegram channel before you pay for an ad in it.** Give this Actor one channel or a list of
them and it returns one row per channel with the numbers an ad buyer asks for: the **exact subscriber
count**, the **median views per post**, **reach % (ERR)**, **posts per day**, **how many posts were
labelled ads** and how those ads were read compared with the other posts, the **best hour and weekday**
to post, the **advertising contact** named in the channel description, and **ranks** across every
channel in the run.

**Every number says what it was measured on.** Each row carries `postsUsed`, `periodFrom`,
`periodTo` and a plain-English `basisSummary` such as *"20 post(s) from 2026-01-09 to 2026-09-03 (the
channel has no older posts). View figures use 20 post(s) older than 24 h."* An average without its
basis is not something you can compare, so this Actor does not return one.

**The subscriber count is the exact one.** Telegram's preview page shows a rounded figure - Bloomberg's
channel reads **172K** there - while the channel's own profile page publishes **171,899**. Reach % is
calculated only against that exact figure; when it cannot be read, reach % is left empty and
`subscribersNote` says why, instead of dividing by a rounded number.

No login, no Telegram API key, no account. Only the pages Telegram serves to anyone without logging
in are read. Export as JSON, CSV or Excel.

### What one channel row gives you

| Column | What it is |
|---|---|
| `channel`, `channelTitle`, `channelUrl`, `verified`, `description` | the channel |
| `adContactHandles` | @usernames named next to "реклама", "ads", "сотрудничество", "promo" and similar words in the description - who to contact to buy an ad |
| `subscribers`, `subscribersAreExact`, `subscribersDisplay`, `subscribersNote` | the exact count from the profile page, the rounded figure the preview shows, and why the exact count is missing when it is |
| `postsUsed`, `periodRequestedDays`, `periodFrom`, `periodTo`, `daysCovered`, `postsLimitedBy`, `basisSummary` | which posts the figures were measured on, and why reading stopped where it did |
| `viewsMedian`, `viewsMean`, `viewsMin`, `viewsMax`, `postsWithViewsCounted`, `postsTooNewForViews` | views per post, from posts old enough for their views to have settled |
| `errPct`, `errMedianPct` | reach %: mean (and median) views per post divided by the exact subscriber count |
| `reactionsMedian`, `reactionsPer1kViews` | how much readers react |
| `postsPerDay`, `postsPerWeek`, `medianHoursBetweenPosts`, `lastPostAt`, `daysSinceLastPost` | posting rhythm, and whether the channel has gone quiet |
| `viewsTrendPct`, `cadenceTrendPct` (with the medians and counts behind them) | is the channel growing or fading within the period: newer half against older half |
| `adPosts`, `adPostsPct`, `adPostsPerWeek`, `adMarkersFound`, `adPostUrls` | how many posts carried an ad label, which labels, and links to those posts |
| `adViewsMedian`, `otherViewsMedian`, `adViewsVsOtherPct` | how ads were read compared with the channel's other posts - the number to look at before you buy |
| `forwardedPosts`, `forwardedPct`, `topForwardSources`, `topMentionedChannels` | which channels it reposts and mentions (useful for finding similar channels) |
| `photoPct`, `videoPct`, `linkPreviewPct`, `textOnlyPct` | the content mix |
| `timezone`, `bestHour`, `bestWeekday`, `postingHours[]`, `postingWeekdays[]` | posts and median views by hour and weekday, in the time zone you choose |
| `topPosts[]` | the five most-viewed posts with their links |
| `channelsCompared`, `rankBySubscribers`, `rankByViewsMedian`, `rankByErr`, `rankByPostsPerDay`, `rankByFewestAds` | ranks across the channels in the same run (1 = best; ties share a rank) |

Turn on `includePosts` to also get one row per post used (`rowType: "post"`) with its views,
reactions, `isAd`, the ad labels found in it and `countedInViewFigures`, so any figure in the channel
row can be checked post by post.

### How ads are recognised

Only by the labels written in the post - nothing is guessed from the topic or the wording:

- the Russian ad registration id **erid**, written in the text or carried in a link (`?erid=...`)
- the legal label line **"Реклама."** / **"Реклама,"** at the start of a line, **"на правах рекламы"**,
  **"партнёрский материал"** / **"партнерский пост"**
- **"sponsored post"**, **"paid partnership"**, a line that says only "Sponsored" or "Advertisement"
- the hashtags **#реклама, #ad, #ads, #sponsored, #advertising, #promo, #промо, #партнер, #partner, #спонсор**
- any words you add in `extraAdMarkers`

Checked on a real feed: on the vc.ru news channel, 2 of 19 posts on one page carried an erid link and
both were found, and none of the other 17 were marked. A news post that merely mentions advertising
("Зрителей достала реклама в телевизоре") and a description line that names the ad contact
("Реклама: @name") are **not** counted as ads.

### Input

| Field | What it does |
|---|---|
| `channels` | the public channels to analyze, as `@durov`, `durov`, `https://t.me/durov` or a link to a single post |
| `periodDays` | measure the posts of the last N days (default 30; 0 = the most recent posts up to the maximum) |
| `maxPostsPerChannel` | how far back each channel is read at most (default 300; Telegram serves 20 posts per page) |
| `settleHours` | leave posts younger than this out of the view figures (default 24) |
| `timezone` | the IANA time zone for the hour and weekday tables (default UTC) |
| `includePosts` | also return one row per post used |
| `extraAdMarkers` | extra words that mark a post as an ad on a particular channel |
| `useProxy` | retry through a proxy if Telegram ever answers with a bot check |

### Rows that are never charged

When there is nothing to sell, you get a row that says why, and it is free:

| `status` | What happened |
|---|---|
| `no-such-channel` | nobody has taken the name. Telegram answers a free name with its generic download page instead of a profile |
| `not-a-public-channel` | the name exists, but has no public post preview: Telegram answers the preview URL with a **302**. It does that for a bot, a personal account, a group, and a channel whose owner turned the preview off, so this row does not claim which |
| `no-results` | the channel page loaded and Telegram published no posts on it |
| `blocked` | Telegram answered with a bot check instead of the page |
| `unreadable` | the page could not be read, or the input was not a public channel name; nothing is guessed |
| `duplicate-channel` | the same channel is listed twice, or the name is another username for a channel already analyzed in this run (`t.me/techmeme` and `t.me/arstechnica` once led to the same channel), so it is not charged twice |
| `budget-reached` | the run hit the maximum total charge you set and stopped; the row says how many rows were left behind |

A channel that exists but posted nothing in the period **does** get its analysis row: subscribers,
`lastPostAt` and `daysSinceLastPost` tell you it has gone quiet, which is worth knowing before buying.

### Limitations

- **View counts are rounded by Telegram** to three significant figures (`24.2M`, `53.1K`) on the
  public pages, and there is no exact view count published anywhere without logging in. Median views
  and reach % are therefore close approximations (the rounding is at most about 0.5% of a value);
  every row says so in `viewsAreRounded`.
- **Ads without a label are not detected.** A native post written as ordinary content with no erid,
  legal label or ad hashtag counts as a normal post. `adPostsPct` is the share of *labelled* ads.
- Share counts and the list of who forwarded a post are not public, so they are not returned.
  `topForwardSources` counts the other way round: which channels this channel reposts.
- Subscriber history (growth over months) is not on the public pages; trends here are measured from
  the posts in the period you choose.
- `subscribers` is filled only when Telegram labels the figure "subscribers". A group that reports
  "members" leaves it empty rather than passing off a different number as subscribers.
- Very active channels can post more than `maxPostsPerChannel` within the period. The row then says
  `postsLimitedBy: "max-posts"`, and the figures cover the dates in `periodFrom` to `periodTo`.
- `bestHour` and `bestWeekday` are filled only when at least two hours (or weekdays) each have three
  or more settled posts and one is strictly ahead; a tie is left empty rather than picking the earlier
  one.
- Invite links (`t.me/+...`, `t.me/joinchat/...`) and `t.me/c/...` are private entrances. They are
  rejected rather than read.
- Email addresses and phone numbers in descriptions and post text are replaced with
  `[email removed]` / `[phone removed]`.

### Connection

Measured on 2026-10-19: `t.me/robots.txt` answers **404** - nothing is declared as disallowed - and
the public preview pages answered directly from Apify, so no proxy is used by default. Each page is
requested with a pause between requests, and only as many pages as the period needs.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a number you check before buying an ad?** Tell us in the **Issues** tab. If the data is public, we add it.
- **Found a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

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

Public channels as @durov, durov, https://t.me/durov or https://t.me/s/durov. A link to a single post (t.me/durov/527) works too - the channel is taken from it. Put several channels in one run to get them ranked against each other. Only channels with a public web preview can be read without logging in; invite links (t.me/+..., t.me/joinchat/...) and t.me/c/... are private entrances and are rejected rather than read. A name that has no public preview comes back as its own row that says so, and is not charged.

## `periodDays` (type: `integer`):

Measure the posts published in the last N days. Every row says how many posts were used and the dates of the first and last one, and whether the whole period was read or reading stopped earlier (the channel is younger, or the maximum posts per channel was reached). 0 = no period: use the most recent posts up to the maximum below.

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

A cap on how far back each channel is read. Telegram serves 20 posts per page. Very active channels can post more than this within the period; the row then says the period was not fully read. You are charged per channel row, not per post read.

## `settleHours` (type: `integer`):

Telegram view counts keep growing for a while after a post goes out, so a post published an hour ago pulls the median down. Posts younger than this are left out of every view figure, and each row counts how many were left out. 0 = use every post.

## `timezone` (type: `string`):

An IANA time zone name such as UTC, Europe/Moscow, Asia/Tashkent or America/New_York. The posting-hour table, the best hour and the best weekday are counted in this zone. A name that is not a real time zone stops the run with a note row that is not charged.

## `includePosts` (type: `boolean`):

Off = one analysis row per channel. On = also one row per post used in the analysis, with its views, reactions, whether it was counted in the view figures and which ad markers were found in it, so you can check any number in the channel row yourself. Post rows are charged separately at the per-post price.

## `extraAdMarkers` (type: `array`):

Ads are recognised only by the labels written in the post: the Russian ad registration id (erid, in the text or in a link), the legal 'Реклама.' line, 'на правах рекламы', 'партнёрский материал' / 'партнерский пост', 'sponsored post', and hashtags such as #реклама, #ad and #promo. Add the words a particular channel uses for its paid posts (for example 'промокод' or 'партнёр рубрики'). Case is ignored.

## `useProxy` (type: `boolean`):

Off by default because t.me answered with the full page on every direct request measured from Apify. Turn it on to have the run retry through a proxy if Telegram ever answers with a bot check - a residential proxy when your account has one, a datacenter proxy otherwise. A proxy makes each run slower and more expensive, so it is not used unless it is needed.

## Actor input object example

```json
{
  "channels": [
    "@durov",
    "@bloomberg",
    "@telegram"
  ],
  "periodDays": 30,
  "maxPostsPerChannel": 300,
  "settleHours": 24,
  "timezone": "UTC",
  "includePosts": false,
  "extraAdMarkers": [],
  "useProxy": false
}
```

# Actor output Schema

## `results` (type: `string`):

One analysis row per channel (rowType channel): title, description, verified flag, the advertising contact handles named in the description, the exact subscriber count from the profile page, how many posts and which dates the figures were measured on, median, mean, lowest and highest views per post (from posts older than the settle time), reach % (ERR) against the exact subscriber count, reactions per 1,000 views, posts per day and per week, days since the last post, the views and posting trend between the older and newer half of the period, the number and share of posts carrying an ad label with the labels found and links to those posts, ad-post views against other posts, the share of forwarded posts and their top sources, the most mentioned channels, the share of photo, video and link posts, posts and median views by hour and weekday in the chosen time zone with the best hour and weekday, the top posts, and ranks across the channels in the run. With includePosts on, one row per post used (rowType post) follows. Channels with no public preview, names that do not exist, pages that came back as a bot check and duplicates come back as their own rows and are not charged.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "channels": [
        "@durov",
        "@bloomberg",
        "@telegram"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/telegram-channel-ad-analytics").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",
        "@bloomberg",
        "@telegram",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/telegram-channel-ad-analytics").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",
    "@bloomberg",
    "@telegram"
  ]
}' |
apify call neverempty/telegram-channel-ad-analytics --silent --output-dataset

```

## MCP server setup

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

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/aARTgMLg85ApP1lwK/builds/4ZdgbhulzJnv9tmfC/openapi.json
