SFOLN

Catalogue / Social media data

Telegram Channel Scraper – Public Posts

sfoln/telegram-channel-scraper · Social media, News, Lead generation On request

Scrape posts from public Telegram channels without login: text and markdown, date, views, reactions, links, media URLs (photos, videos, files), forwards and replies, plus the channel's title, description and subscriber count. $0.80 per 1,000 posts, channel info free.

About

What it does

Scrape posts from public Telegram channels without a Telegram account, phone number or API key. For each channel you get its newest posts as clean rows — text and markdown, date, views, reactions, links, hashtags, photo/video/file URLs, forwards and replies — plus the channel info (title, description, subscribers, photo, verified badge) for free. Built for news and crypto monitoring, brand and community tracking, research datasets and AI agents.

Price

Price per delivered record

EventPricePer 1,000Charged for
Telegram post
post
$0.0008$0.80 / 1,000Charged once per channel post delivered (text, date, views, reactions, links, media URLs).

Apify free plan: up to 10 items per run. Paid plans have no per-run cap; set maxTotalChargeUsd to cap spend.

Run

Call it from code, an agent or the Console

This API is private on Apify for now. Email us and we will publish it for you, usually within two working days. The calls below work once it is public. contact@sfoln.com

curl · synchronous run, rows in the response
curl -X POST "https://api.apify.com/v2/actors/sfoln~telegram-channel-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"channels":["telegram"],"maxPostsPerChannel":10}'
mcp.json · hosted Apify MCP server
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/telegram-channel-scraper"
    }
  }
}
asynchronous run · for large batches
POST https://api.apify.com/v2/actors/sfoln~telegram-channel-scraper/runs?maxTotalChargeUsd=1
GET  https://api.apify.com/v2/actor-runs/{runId}            # until status is SUCCEEDED
GET  https://api.apify.com/v2/datasets/{defaultDatasetId}/items?format=json

Authenticate with an Apify API token. MCP clients can instead sign in with OAuth when prompted. Example prompt for an agent: Use telegram-channel-scraper to get the last 20 posts of the telegram channel and summarize what was announced, with dates and view counts.

Input

Input fields

Public Telegram channels to read. Each channel delivers its newest posts first (one charged row per post) and one free summary record with the channel info.

FieldTypeDefaultMeaning
channels *arrayrequiredChannels
Public channel usernames or links, one per line: telegram, @telegram, https://t.me/telegram or https://t.me/s/telegram (a post link works too). Private channels and invite links (t.me/+…) have no public preview. Accounts on the Apify free plan can process up to 10 channels per run, with up to 20 posts each.
maxPostsPerChannelinteger20Max posts per channel
Newest posts to deliver per channel (Telegram serves 20 per page). Range 1–5000.
postedAfterstring—Posted after
Only posts on or after this date: YYYY-MM-DD, an ISO date-time, or a relative period such as "7 days". Paging stops at the first older post.
beforePostIdinteger—Start before post ID
Start with the posts older than this post ID (e.g. the oldestPostIdSeen of an earlier run) to continue a backfill. Range 2–….
maxChannelsinteger100Max channels
Safety cap on channels in this run; the rest are ignored and counted as inputItemsDropped in the OUTPUT record. Range 1–1000.
concurrencyinteger2Concurrency
Channels read in parallel (requests to t.me are rate limited for the whole run). Range 1–5.
includeRawbooleanfalseInclude raw HTML
Attach each post's HTML block from the public preview page (larger output).
Example input
{
  "channels": [
    "telegram"
  ],
  "maxPostsPerChannel": 10
}

Output

One record per input item

Records land in the run's dataset; a synchronous call returns them directly. Each has a status (ok is charged; everything else is free) and a charged flag. The run's OUTPUT record totals items and charges.

FieldTypeMeaning
inputstring | objectInput
The channel this record belongs to (canonical username).
recordTypestringRecord type
post (charged) or summary (one per channel, with the channel info; free). e.g. "post"
positionintegerPosition
Post number within its channel in this run (1 = newest).
statusstringStatus
ok | not_found | error | skipped_invalid | skipped_budget | skipped_free_plan | skipped_unmetered e.g. "ok"
chargedbooleanCharged
Whether this record incurred a pay-per-event charge.
chargedEventstringCharged event
Pay-per-event event charged for this record (present only when charged).
chargedCountintegerEvents charged
Events actually charged when the charge came back short of the requested count (present only then; a warning explains it).
sourcestringSource
Upstream data source.
fetchedAtstringFetched at
ISO timestamp.
errorobjectError
{ code, message, provider?, status? } when status is not ok. Codes set by the runtime: budget_exhausted, free_plan_limit, unmetered, aborted, charge_failed, record_too_large, invalid_record, dataset_push_failed; upstream codes: rate_limit, timeout, network, server, client, invalid_response, not_found; this Actor: invalid_input.
warningsarrayWarnings
Notes about this record, e.g. a cap or the budget that stopped a channel early.
channelstringChannel
Channel username (lower case).
postIdintegerPost ID
Message id within the channel.
urlstringPost URL
https://t.me/<channel>/<postId>
datestring | nullDate
Publication time, ISO 8601 UTC.
editedbooleanEdited
The post was edited after publication.
authorstring | nullAuthor signature
Author name shown on the post, when the channel signs posts.
textstring | nullText
Plain text of the post or caption.
markdownstring | nullMarkdown
The same text with bold, italic, links, code and quotes as markdown.
viewsinteger | nullViews
View count (approximate when Telegram abbreviates it).
viewsTextstring | nullViews (as shown)
The view count as Telegram shows it, e.g. 1.56M.
reactionsarrayReactions
[{ emoji, count }]: the emoji, paid (Telegram Stars) or custom:<id> for a custom emoji.
reactionsTotalinteger | nullReactions total
Sum of all reaction counts.
forwardedFromobject | nullForwarded from
{ name, url, username } of the original sender of a forwarded post (fields are null when hidden).
replyToobject | nullReply to
{ postId, url, author, text } of the post this one replies to.
linksarrayLinks
Links in the text and the link preview.
hashtagsarrayHashtags
#hashtags in the text.
mentionsarrayMentions
@mentions in the text.
linkPreviewobject | nullLink preview
{ url, siteName, title, description, imageUrl } of the attached link preview.
pollobject | nullPoll
{ question, type, options: [{ text, percent }] }.
mediaarrayMedia
[{ type, url, thumbnailUrl, postUrl, durationSecs, title, details }]; type: photo, video, round_video, document, audio, voice, sticker, location.
mediaTypesarrayMedia types
Distinct media types of the post.
mediaNotSupportedbooleanMedia only in the app
Telegram shows this post's media only in its apps.
channelTitlestring | nullChannel title
The channel's display name.
rawstringRaw HTML
The post's HTML block from the preview page (only with includeRaw).
channelDescriptionstring | nullChannel description
Summary records: the channel's description.
subscribersinteger | nullSubscribers
Summary records: subscriber count (approximate when abbreviated).
subscribersTextstring | nullSubscribers (as shown)
Summary records: e.g. 9.33M.
photoUrlstring | nullChannel photo
Summary records: channel photo URL.
verifiedbooleanVerified
Summary records: the channel has Telegram's verified badge.
countersobjectCounters
Summary records: other header counters (photos, videos, files, links).
channelUrlstring | nullChannel URL
Summary records: https://t.me/<channel>.
oldestPostIdSeeninteger | nullOldest post ID seen
Summary records: the oldest post ID this run delivered (null when none); pass it as beforePostId to continue further back.
resultsDeliveredintegerPosts delivered
Summary records: charged post rows of this channel.
stopReasonstringStop reason
Summary records: limit | free_plan | end | budget | aborted | deadline | empty_pages | max_pages | error.
Example record
[
  {
    "input": "synthetic_news",
    "recordType": "post",
    "position": 1,
    "channel": "synthetic_news",
    "postId": 101,
    "url": "https://t.me/synthetic_news/101",
    "date": "2026-10-10T08:00:00.000Z",
    "edited": true,
    "author": "Synthetic Editor",
    "text": "✨ Big update is out: read the release notes.",
    "markdown": "✨ **Big update** is out: read the [release notes](https://example.com/blog/update).",
    "views": 15300,
    "viewsText": "15.3K",
    "reactions": [
      {
        "emoji": "👍",
        "count": 1200
      },
      {
        "emoji": "paid",
        "count": 12
      }
    ],
    "reactionsTotal": 1212,
    "forwardedFrom": null,
    "replyTo": null,
    "links": [
      "https://example.com/blog/update"
    ],
    "hashtags": [
      "#release"
    ],
    "mentions": [],
    "linkPreview": {
      "url": "https://example.com/blog/update",
      "siteName": "Example Blog",
      "title": "Release notes",
      "description": "Everything new in this release.",
      "imageUrl": "https://cdn4.example-cdn.test/file/preview.jpg"
    },
    "poll": null,
    "media": [
      {
        "type": "photo",
        "url": null,
        "thumbnailUrl": "https://cdn4.example-cdn.test/file/photo-101.jpg",
        "postUrl": "https://t.me/synthetic_news/101",
        "durationSecs": null,
        "title": null,
        "details": null
      }
    ],
    "mediaTypes": [
      "photo"
    ],
    "mediaNotSupported": false,
    "channelTitle": "Synthetic News",
    "status": "ok",
    "charged": true,
    "chargedEvent": "post",
    "source": "telegram",
    "fetchedAt": "2026-10-14T12:00:00.000Z"
  },
  {
    "input": "synthetic_news",
    "channel": "synthetic_news",
    "channelTitle": "Synthetic News",
    "channelDescription": "Test headlines for a synthetic channel.",
    "subscribers": 1250000,
    "subscribersText": "1.25M",
    "photoUrl": "https://cdn1.example-cdn.test/file/channel-photo.jpg",
    "verified": true,
    "counters": {
      "photos": 412,
      "videos": 38,
      "links": 2100
    },
    "channelUrl": "https://t.me/synthetic_news",
    "oldestPostIdSeen": 82,
    "recordType": "summary",
    "resultsDelivered": 20,
    "stopReason": "limit",
    "status": "ok",
    "charged": false,
    "source": "telegram",
    "fetchedAt": "2026-10-14T12:00:02.000Z"
  }
]

FAQ

Questions

Do I need a Telegram account, phone number or API key?
No. The Actor reads the public web preview Telegram shows for every public channel at `t.me/s/<channel>`; no login is involved.
Which channels work?
Public channels with a username. Private channels, invite links (t.me/+…), groups, bots and user accounts have no public preview and come back as not_found (free).
Are view and subscriber counts exact?
Telegram rounds them on the preview (for example 1.56M). The Actor returns the number it shows (views: 1560000) next to the original text (viewsText: "1.56M").
How do I scrape only new posts every day?
Schedule the Actor with postedAfter set to a relative period such as "1 day", or keep the oldestPostIdSeen of a run and pass it as beforePostId to continue a backfill.

Machine-readable

This page for agents