# Telegram Media Scraper — Photos & Video Links (`apt_marble/telegram-media-scraper`) Actor

Every photo and video from any public Telegram channel, one row per file, with a direct link you can download. Albums are fully expanded — all ten images, not just the first. Videos arrive with a playable link, poster frame and duration. No Telegram account or login. Export to CSV, Excel or JSON.

- **URL**: https://apify.com/apt\_marble/telegram-media-scraper.md
- **Developed by:** [Hamza](https://apify.com/apt_marble) (community)
- **Categories:** Social media, Videos
- **Stats:** 1 total users, 0 monthly users, 97.1% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.20 / 1,000 media items

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 Media Scraper — Photos & Video Links

Point it at any public Telegram channel and get every photo and video it has published, one row per file, each with a direct link you can download. Photo albums are fully expanded — a post carrying ten images gives you ten rows, not one. Videos arrive with a playable link, a poster frame and a duration in seconds. Nothing to install, no Telegram account, no login: paste channel names, press start, download the result as CSV, Excel or JSON.

### What you can do with it

- **Build an image library from a channel** — every photo it has published, ready to download in bulk.
- **Collect video clips with their details** — playable link, poster frame, duration and the caption they were posted with.
- **Feed a brand-safety or computer-vision pipeline** — a clean list of image and video links, refreshed on a schedule.
- **Archive visual coverage from news channels** — pictures of an event as they were published, with the exact publication time.
- **See what a competitor posts and how it lands** — each attachment carries the views and reactions of the post it came from.
- **Audit a channel's media mix** — how much of it is photos, how much is video, how long the videos run.

### What you get

One row per photo or video, not one row per post. Abridged real example — the second image of a six-image album:

```json
{
  "channel": "bbcrussian",
  "channelTitle": "BBC News | Русская служба",
  "channelId": -1003921752,
  "subscriberCount": 389000,
  "subscriberCountIsExact": false,
  "mediaId": "bbcrussian/98011#2",
  "postId": 98011,
  "mediaMessageId": 98012,
  "date": "2026-08-04T16:48:19.000Z",
  "mediaType": "photo",
  "mediaGroup": "photo",
  "fileUrl": "https://…/file/tkZKiv0VTadzUYNPZY9vUqyEEi_c-ZxeOHjLIMKcxlz.jpg",
  "fileExtension": "jpg",
  "isDownloadable": true,
  "thumbnailUrl": null,
  "duration": null,
  "durationSeconds": null,
  "unavailableReason": null,
  "isAlbum": true,
  "positionInPost": 2,
  "itemsInPost": 6,
  "caption": "📸 Фото дня. Извержение вулкана Фуэго в Гватемале …",
  "viewCount": 29000,
  "viewCountIsExact": false,
  "viewCountPublished": "29K",
  "reactionCount": null,
  "reactionCountIsExact": null,
  "isForwarded": false,
  "hashtags": [],
  "scrapedAt": "2026-08-05T09:14:22.000Z"
}
```

A video row looks the same, with `fileUrl` pointing at a playable file, `thumbnailUrl` at the poster frame and `durationSeconds` filled in.

### Input reference

Every setting below appears in the actor's form, labelled exactly as shown.

| Setting | Type | Default | What it does |
| --- | --- | --- | --- |
| Telegram channels | list of strings | — | The public channels to collect from. A channel link or an `@username`, one per line, up to **1,000** in one run — anything past that is left out and named in the run log. **Required.** |
| Media to collect | multi-select | Photos, Videos | Which attachments to return. Photos and videos come with a direct file link. Files, audio and voice notes can be added, but Telegram publishes no downloadable link for them, so those rows carry their title and kind only. |
| Posts to look through per channel | integer | `200` | How many recent posts to look through in each channel, newest first. Posts with no attachments produce no rows, so a text-heavy channel returns fewer items than posts. Maximum 20,000. |
| Maximum media items | integer | `1000` | A ceiling on the rows the whole run may produce, across every channel. The run stops cleanly when it is reached and keeps everything already collected. Maximum 200,000. |
| Browse from | select | `United States` | The country to browse from **on a second attempt**, used only when Telegram declines the first one. Most runs never need it. A few channels are restricted in some countries, so on that second attempt it can change what a channel returns. |
| Parallel workers | integer | `16` | How many channels are worked through at the same time. Maximum 32. |

### Output fields

| Field | Type | Description |
| --- | --- | --- |
| `channel` | string | The channel's `@username`, without the `@`. |
| `channelTitle` | string | null | The channel's display name. |
| `channelId` | number | null | Telegram's own numeric id for the channel. One channel can answer on more than one name, so this is the reliable way to group rows. |
| `channelUrl` | string | Public link to the channel. |
| `subscriberCount` | number | null | Subscribers at the moment of collection. |
| `subscriberCountIsExact` | boolean | null | `false` when Telegram published the figure in rounded form. |
| `mediaId` | string | A stable id for this exact attachment: channel, post and the attachment's place inside it. |
| `postId` | number | The post the attachment belongs to. For an album this is the post's first number. |
| `postUrl` | string | Public link to the post. |
| `mediaMessageId` | number | null | The attachment's own number, when Telegram gives it one — every image and every video in an album has its own. `null` when it does not have one. |
| `mediaUrl` | string | Public link to the individual attachment, or to its post when it has no link of its own. |
| `date` | string | null | When the post was published, in UTC. |
| `mediaType` | string | `photo`, `video`, `round_video`, `sticker`, `audio`, `voice`, `file`, or `unavailable` for media Telegram does not publish outside its own apps. |
| `mediaGroup` | string | The family the row belongs to: `photo`, `video` or `document`. |
| `fileUrl` | string | null | Direct link to the file. `null` when Telegram publishes none — see `unavailableReason`. |
| `fileExtension` | string | null | The file's format, e.g. `jpg`, `mp4`, `webp`. |
| `isDownloadable` | boolean | `true` when the row has a direct file link. |
| `thumbnailUrl` | string | null | Poster frame for a video. |
| `duration` | string | null | Video length as Telegram displays it, e.g. `"0:42"`. |
| `durationSeconds` | number | null | The same length in seconds. |
| `fileTitle` | string | null | Title of a file, audio track or voice note. |
| `filePerformer` | string | null | Performer or subtitle of an audio track. |
| `fileSize` | string | null | Size of a plain file as Telegram displays it. |
| `unavailableReason` | string | null | Why a row has no file: `no_file_published` for files, audio and voice notes, `not_viewable_outside_telegram` for media Telegram keeps inside its own apps. |
| `isAlbum` | boolean | `true` when the attachment came from a multi-image post. |
| `positionInPost` | number | This attachment's place in its post, starting at 1, in the order Telegram published them — so a post mixing pictures and video keeps the order it was posted in. |
| `itemsInPost` | number | How many attachments the post carried in total. |
| `caption` | string | null | The text of the post the attachment belongs to. `null` when the post had none. |
| `viewCount` | number | null | Views of the post at the moment of collection. `null` when the channel does not publish views. |
| `viewCountIsExact` | boolean | null | `false` when the figure was published in rounded form. |
| `viewCountPublished` | string | null | Exactly what Telegram displayed, e.g. `"29K"`. |
| `reactionCount` | number | null | All reactions on the post added together. `null` when the channel publishes no reactions at all. |
| `reactionCountIsExact` | boolean | null | `false` when at least one reaction was published in rounded form, so the total is an approximation. `null` when there are no reactions to add up. |
| `isForwarded` | boolean | Whether the post was forwarded from somewhere else. |
| `forwardedFrom` | object | null | Where it came from: name, link and `@username`. |
| `hashtags` | array | Hashtags in the caption, without the `#`. |
| `scrapedAt` | string | When this row was collected, in UTC. |

### Pricing

You pay per result, not for time.

| What you pay for | Price |
| --- | --- |
| Every media item collected | **$1.20 per 1,000 items** |

Nothing else is billed. A post with no attachments produces no row and costs nothing, and a channel that turns out to be unavailable costs nothing at all.

**Worked example.** You sweep 10 photo-heavy channels, looking through their latest 200 posts each, and get about 1,500 photos and videos. That is **$1.80** for the run. A daily watch on the same 10 channels at 50 posts each returns roughly 350 items — about **$0.42 a day**. Set **Maximum media items** to put a hard ceiling on any single run, or cap the spend on the run and the actor stops cleanly at the cap, keeping everything already collected.

### Limits & what this actor cannot do

- Only **public** Telegram channels are covered. Private channels, invite-only groups and one-to-one chats are not publicly viewable, and are reported as unavailable rather than returned empty.
- **Groups are not channels.** A public group's messages are not published for reading, so a group handle is reported as unavailable rather than returned empty.
- **Files, audio and voice notes have no downloadable link.** Telegram publishes their title, performer and kind and nothing more. Those rows say so in `unavailableReason` instead of being dropped or given a fake link.
- Some posts carry media Telegram **does not make viewable outside its own apps**. Those rows are returned with the poster frame, the duration and `unavailableReason` set, so the post is never silently missing from your data.
- Photo and video links point at Telegram's own storage. Treat them as fresh at collection time and download soon after a run rather than months later.
- View, reaction and subscriber counts are a **snapshot** at the moment of collection and keep changing afterwards.
- Telegram publishes very large counts in **rounded** form, so the biggest numbers are approximate. Every row says which of its numbers were rounded, so you never have to guess.
- **Absent is not zero.** Some channels publish no view counts and some publish no reactions at all. Those fields arrive as `null`, which means "Telegram never published this" — not "this post got none".
- A run only looks through the number of recent posts you set per channel, so anything older is left alone until you raise that number.
- **One channel can answer on two different names.** When two of the names you paste turn out to be the same channel, it is collected once and the run log names the pair — you are never charged twice for the same file.
- Occasionally a channel stops being readable part-way through a run. It keeps everything collected up to that point and the run log says the channel **stopped early**, so a truncated channel is never presented as a complete one.
- Some channels are **restricted in some countries**. The country setting is only used on a second attempt, after Telegram declines the first, so most runs are unaffected by it.
- Speed depends on the size of the job and on Telegram's own response times; no fixed throughput is promised.
- Telegram's terms govern automated access. You are responsible for using the data lawfully, in line with the source site's terms, and for handling any personal data in line with applicable privacy law.

### FAQ

**Do I need a Telegram account?**
No. Nothing is connected to any account of yours.

**Does it need my phone number or a login?**
No. There is nothing to sign in to and nothing to authorise. You paste channel names and press start.

**Can I schedule it?**
Yes. Schedule it on Apify to run hourly, daily or weekly, and each run writes a fresh dataset you can export or send onward.

**Is the data complete?**
It is complete for what Telegram publishes publicly. Every image in an album is returned, not just the first. What it cannot give you is a file for attachments Telegram publishes no file for — files, audio, voice notes, and the occasional post whose media stays inside Telegram's own apps. Those are returned and marked, never quietly skipped.

**Are photo albums one row or many?**
Many — one row per image, each with its own link and its own place in the album (`positionInPost` of `itemsInPost`). Grouping rows by `postId` puts an album back together.

**Can I actually download the files?**
Yes. Every row where `isDownloadable` is `true` carries a direct link to the file itself, which any downloader or spreadsheet tool can fetch.

**Why do some rows have no file link?**
Because Telegram publishes none for that kind of attachment. `unavailableReason` says which case it is, and the row still carries the title, the caption, the timing and the engagement of the post it belongs to.

**How do I get only videos?**
Set **Media to collect** to Videos alone. Photos and files are then left out entirely and you are not billed for them.

**What happens if I paste a channel that does not exist?**
It is reported as unavailable, with the reason, and the rest of the run continues. You are never charged for it.

# Actor input Schema

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

The public channels to collect media from. Paste a channel link or an @username — one per line, up to 1,000 in a single run. Private channels, invite links and groups have no public post history and are reported as unavailable. One channel sometimes answers on two different names; it is collected, and charged for, only once.

## `mediaTypes` (type: `array`):

Which attachments to return. Photos and videos come with a direct file link. Files, audio and voice notes are returned with their title and kind only — Telegram publishes no downloadable link for them — so they are switched off by default.

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

How many recent posts to look through in each channel, newest first. Posts with no attachments produce no rows, so a text-heavy channel returns fewer items than posts. Raise it to reach further back.

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

A hard ceiling on the number of rows the whole run may produce, across every channel. The run stops cleanly when it is reached and keeps everything already collected.

## `country` (type: `string`):

The country to browse from on a second attempt. It is used only when Telegram declines the first attempt at a channel, which is rare — a run where every channel answers first time is unaffected by this setting. A few channels are restricted in some countries, so on that second attempt it can change what a channel returns.

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

How many channels to work through at the same time. 16 is a good balance of speed and politeness; raise it for very large jobs.

## Actor input object example

```json
{
  "channels": [
    "@bbcrussian",
    "https://t.me/durov"
  ],
  "mediaTypes": [
    "photo",
    "video"
  ],
  "maxPostsPerChannel": 100,
  "maxItems": 200,
  "country": "us",
  "maxConcurrency": 6
}
```

# Actor output Schema

## `dataset` (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 = {
    "channels": [
        "@bbcrussian",
        "@durov"
    ],
    "mediaTypes": [
        "photo",
        "video"
    ],
    "maxPostsPerChannel": 100,
    "maxItems": 200,
    "country": "us",
    "maxConcurrency": 6
};

// Run the Actor and wait for it to finish
const run = await client.actor("apt_marble/telegram-media-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": [
        "@bbcrussian",
        "@durov",
    ],
    "mediaTypes": [
        "photo",
        "video",
    ],
    "maxPostsPerChannel": 100,
    "maxItems": 200,
    "country": "us",
    "maxConcurrency": 6,
}

# Run the Actor and wait for it to finish
run = client.actor("apt_marble/telegram-media-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": [
    "@bbcrussian",
    "@durov"
  ],
  "mediaTypes": [
    "photo",
    "video"
  ],
  "maxPostsPerChannel": 100,
  "maxItems": 200,
  "country": "us",
  "maxConcurrency": 6
}' |
apify call apt_marble/telegram-media-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,apt_marble/telegram-media-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/bmnuq20JeP47rk1tI/builds/91UN0zjRTL7L8ztHK/openapi.json
