Building a Telegram News Bot with Python (2026 Edition)
A Telegram news bot is a Python program that fetches articles from a news API, filters them by topic or sentiment, and posts them to a Telegram chat or channel automatically. In 2026 the fast way to build one is aiogram 3.27 for the async Telegram client, APScheduler for in-process scheduling, and a news API that exposes sentiment and entities so you can post curated streams instead of a firehose.
Key takeaways
- Use
aiogram3.27 (async) overpython-telegram-botfor channel auto-posting bots above ~50 concurrent users.- Filter the news API response by
sentiment.overall.polarityandentities[]so the channel posts curated content, not a firehose.APScheduler'sAsyncIOSchedulerruns inside the aiogram event loop — one process for polling, fetching, and scheduled posting.- Ship as a multi-stage Python 3.12 Docker image with
restart: unless-stopped; Fly.io, Railway, or a $5/month VPS all work.
This tutorial walks through a production-shaped channel bot in about 100 lines: it pulls articles every 15 minutes, drops anything that doesn't match the sentiment and entity filter, deduplicates by article ID, and survives a container restart. By the end you'll have a Dockerized service you can drop on any VPS.
What you'll build
- An async Telegram bot that posts news to a channel automatically
- Sentiment + entity filter (e.g., only post negative articles mentioning Tesla)
- APScheduler running inside the aiogram event loop — no cron, no second process
- Deduplication by article ID and retry on Telegram rate limits
- A multi-stage Dockerfile with
restart: unless-stoppedyou can ship today
aiogram vs python-telegram-bot — quick decision
This is the question every Python dev asks first. Both libraries are maintained in 2026; the right pick depends on workload.
| Criterion | aiogram 3.27 | python-telegram-bot 21.x |
|---|---|---|
| Concurrency model | Async (asyncio) only | Async since v20; sync wrapper still available |
| Latency at 100 concurrent updates | ~80 ms p95 | ~600 ms p95 (sync) / ~120 ms (async) |
| Channel auto-post + scheduler in one process | Native fit (AsyncIOScheduler) | Works but more boilerplate |
| FSM, middleware, filters | First-class | First-class |
| Best for | Channel/group bots, news/notification streams, >500 users | Personal bots, command-driven workflows, gradual sync→async migration |
| Community trajectory in 2026 | Faster moving, channel-bot focus | Larger ecosystem, more legacy examples |
Unlike python-telegram-bot, aiogram has been async-only since v3, which means it shares a single event loop with AsyncIOScheduler and httpx.AsyncClient — no bridging, no thread pools, no callback wrapping. Practical rule: under 50 concurrent updates either library is fine; above 500, or for any channel auto-poster, pick aiogram. We'll use aiogram for the rest of this tutorial.
Prerequisites
- Python 3.10 or newer (3.12 recommended)
- A Telegram bot token from
@BotFather - A Telegram channel where your bot is added as an administrator with "Post messages" permission
- An APITube API key from apitube.io — free tier includes sentiment and entities on every article (disclosure: I work on APITube)
- Familiarity with
async/await— ifawait client.get(...)looks alien, skim the Python asyncio tutorial first
How to build a Telegram news bot in Python (6 steps)
- Get a bot token from BotFather
- Set up the project and install dependencies
- Fetch news from APITube (async)
- Filter by sentiment and entities
- Schedule channel posts with APScheduler
- Add
/start,/subscribe,/filtercommands
Step 1 — Get a bot token from BotFather
Open Telegram, message @BotFather, send /newbot, pick a name and a username. You'll get a token that looks like 8123456789:AAH.... Save it.
Then create a channel (or use an existing one), open its settings → Administrators → Add Administrator → search your bot's username → grant "Post messages". Get the channel ID by forwarding any channel message to @JsonDumpBot — you want the chat.id, which for channels is a negative number like -1001234567890.
Step 2 — Set up the project
mkdir telegram-news-bot && cd telegram-news-bot
python -m venv .venv && source .venv/bin/activate
pip install aiogram==3.27.0 apscheduler==3.10.4 httpx==0.27.0 python-dotenv==1.0.1
Create .env:
BOT_TOKEN=8123456789:AAH...
CHANNEL_ID=-1001234567890
APITUBE_KEY=your_apitube_key
Step 3 — Fetch news from APITube
APITube's GET /v1/news/everything endpoint takes a search title, language, category, and date range, and returns articles enriched with sentiment scores and named entities. Auth goes in the X-API-Key header.
Quick sanity check from the shell:
curl "https://api.apitube.io/v1/news/everything?title=Tesla&language.code=en&per_page=2" \
-H "X-API-Key: $APITUBE_KEY"
Response — one full article object, every field APITube returns per result:
{
"status": "ok",
"limit": 2,
"path": "https://api.apitube.io/v1/news/everything?title=Tesla&language.code=en&per_page=2&page=1",
"page": 1,
"has_next_pages": true,
"next_page": "https://api.apitube.io/v1/news/everything?title=Tesla&language.code=en&per_page=2&page=2",
"has_previous_page": false,
"previous_page": "",
"export": {
"json": "https://api.apitube.io/v1/news/everything?title=Tesla&language.code=en&per_page=2&page=1&export=json",
"xlsx": "https://api.apitube.io/v1/news/everything?title=Tesla&language.code=en&per_page=2&page=1&export=xlsx",
"csv": "https://api.apitube.io/v1/news/everything?title=Tesla&language.code=en&per_page=2&page=1&export=csv",
"tsv": "https://api.apitube.io/v1/news/everything?title=Tesla&language.code=en&per_page=2&page=1&export=tsv",
"xml": "https://api.apitube.io/v1/news/everything?title=Tesla&language.code=en&per_page=2&page=1&export=xml",
"rss": "https://api.apitube.io/v1/news/everything?title=Tesla&language.code=en&per_page=2&page=1&export=rss"
},
"request_id": "5978d89c-46f6-4ab4-b65c-8b4a6000d4ac",
"results": [
{
"id": 982451653,
"href": "https://example.com/tesla-recall",
"published_at": "2026-05-02T08:14:00Z",
"title": "Tesla recalls 1.2M vehicles over autopilot flaw",
"description": "The recall covers most Model 3 and Y units...",
"body": "Tesla on Friday recalled roughly 1.2 million vehicles over a defect in its autopilot driver-assistance system. The recall covers most Model 3 and Model Y units built since 2023.",
"body_html": "<p>Tesla on Friday recalled roughly 1.2 million vehicles over a defect in its autopilot driver-assistance system. The recall covers most Model 3 and Model Y units built since 2023.</p>",
"language": "en",
"author": {
"id": 4521,
"name": "Jane Doe"
},
"image": "https://example.com/images/tesla-recall.jpg",
"categories": [
{
"id": 211,
"name": "automotive",
"score": 0.91,
"taxonomy": "iptc_mediatopics",
"links": {
"self": "https://api.apitube.io/v1/news/category/iptc_mediatopics/medtop:20000266"
}
}
],
"topics": [
{
"id": "automotive.recalls",
"name": "Vehicle Recalls",
"score": 0.34,
"links": {
"self": "https://api.apitube.io/v1/news/topic/automotive.recalls"
}
}
],
"industries": [
{
"id": 88,
"name": "Automotive",
"links": {
"self": "https://api.apitube.io/v1/news/industry/88"
}
}
],
"entities": [
{
"id": 8821,
"name": "Tesla",
"type": "organization",
"links": {
"self": "https://api.apitube.io/v1/news/entity/8821",
"wikipedia": "https://en.wikipedia.org/wiki/Tesla,_Inc.",
"wikidata": "https://www.wikidata.org/wiki/Q478214"
},
"frequency": 5,
"title": {
"pos": [
{"start": 0, "end": 5}
]
},
"body": {
"pos": [
{"start": 0, "end": 5},
{"start": 142, "end": 147}
]
},
"metadata": {
"name": "Tesla",
"type": "business",
"country": {"code": "US", "name": "United States"},
"description": "Electric vehicle manufacturer"
}
},
{
"id": 19233,
"name": "Model 3",
"type": "product",
"links": {
"self": "https://api.apitube.io/v1/news/entity/19233"
},
"frequency": 2,
"title": {
"pos": []
},
"body": {
"pos": [
{"start": 58, "end": 65}
]
},
"metadata": {}
}
],
"source": {
"id": 311,
"domain": "reuters.com",
"home_page_url": "https://www.reuters.com",
"type": "news",
"bias": "center",
"rankings": {
"opr": 9
},
"location": {
"country_name": "United States",
"country_code": "us"
},
"favicon": "https://www.reuters.com/favicon.ico"
},
"sentiment": {
"overall": {"score": -0.62, "polarity": "negative"},
"title": {"score": -0.55, "polarity": "negative"},
"body": {"score": -0.64, "polarity": "negative"}
},
"summary": [
{
"sentence": "Tesla recalled roughly 1.2 million vehicles over an autopilot defect.",
"sentiment": {"score": -0.6, "polarity": "negative"}
}
],
"keywords": ["Tesla", "recall", "autopilot", "Model 3"],
"links": [
{"url": "https://example.com/tesla-recall-followup", "type": "link"}
],
"media": [
{"url": "https://example.com/images/tesla-recall.jpg", "type": "image"}
],
"story": {
"id": 3019419820,
"uri": "https://api.apitube.io/v1/news/story/3019419820"
},
"shares": {
"total": 412,
"facebook": 220,
"twitter": 150,
"reddit": 42
},
"is_duplicate": false,
"is_free": true,
"is_breaking": true,
"read_time": 2,
"sentences_count": 8,
"paragraphs_count": 4,
"words_count": 320,
"characters_count": 1820
}
]
}
The bot only touches a handful of these — title, description, href, source.domain, sentiment.overall.polarity, and entities[].name — but everything else (topics, industries, source bias, per-sentence summary, share counts) is there if you want richer posts later.
In Python, async httpx:
import httpx, os
API_URL = "https://api.apitube.io/v1/news/everything"
async def fetch_news(query: str) -> list[dict]:
params = {
"title": query,
"language.code": "en",
"per_page": 20,
"sort.by": "published_at",
"sort.order": "desc",
}
headers = {"X-API-Key": os.environ["APITUBE_KEY"]}
async with httpx.AsyncClient(timeout=15) as client:
r = await client.get(API_URL, params=params, headers=headers)
r.raise_for_status()
return r.json().get("results", [])
Step 4 — Filter by sentiment and entities
This is the step that turns a firehose into a curated channel. Drop articles that don't match the configured polarity, and keep only ones whose entities include at least one watched name. None of the popular tutorials do this — they post everything they fetch.
WATCHED_ENTITIES = {"Tesla", "SpaceX", "OpenAI"}
WANTED_POLARITY = {"negative"} # use {"positive", "negative", "neutral"} to allow all
def passes_filter(article: dict) -> bool:
polarity = article.get("sentiment", {}).get("overall", {}).get("polarity")
if polarity not in WANTED_POLARITY:
return False
names = {e.get("name") for e in article.get("entities", [])}
return bool(names & WATCHED_ENTITIES)
A 30-second filter, but it's the difference between a bot that posts 200 articles a day and one that posts 6 useful ones.
Step 5 — Schedule channel posts with APScheduler
AsyncIOScheduler runs inside the same event loop aiogram is using, so the whole bot is one process. Dedup posted IDs in an in-memory set; for persistence across restarts, swap in Redis or SQLite.
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from aiogram.exceptions import TelegramRetryAfter
import asyncio, html
posted_ids: set[int] = set()
async def post_news(bot, channel_id: int):
articles = await fetch_news("Tesla OR SpaceX OR OpenAI")
for art in articles:
if art["id"] in posted_ids or not passes_filter(art):
continue
text = (
f"<b>{html.escape(art['title'])}</b>\n"
f"{html.escape(art['description'])}\n\n"
f"{art['source']['domain']} · "
f"sentiment: {art['sentiment']['overall']['polarity']}\n"
f"<a href=\"{art['href']}\">Read more</a>"
)
try:
await bot.send_message(channel_id, text, disable_web_page_preview=False)
posted_ids.add(art["id"])
except TelegramRetryAfter as e:
await asyncio.sleep(e.retry_after)
The TelegramRetryAfter catch is what keeps the bot alive when Telegram throttles you for posting too fast — it sleeps the exact requested interval, then the next scheduler tick retries.
Step 6 — Commands and main loop
Three small commands so subscribers can interact with the bot directly:
/start— welcome message/subscribe— explain that the bot auto-posts to the channel/filter— show current filter config
The dispatcher wires them up, and start_polling runs alongside the scheduler.
Complete code (bot.py)
import asyncio, html, logging, os
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.exceptions import TelegramRetryAfter
from aiogram.filters import Command, CommandStart
from aiogram.types import Message
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from dotenv import load_dotenv
import httpx
load_dotenv()
TOKEN = os.environ["BOT_TOKEN"]
CHANNEL_ID = int(os.environ["CHANNEL_ID"])
APITUBE_KEY = os.environ["APITUBE_KEY"]
API_URL = "https://api.apitube.io/v1/news/everything"
WATCHED_ENTITIES = {"Tesla", "SpaceX", "OpenAI"}
WANTED_POLARITY = {"negative"}
posted_ids: set[int] = set()
dp = Dispatcher()
async def fetch_news(query: str) -> list[dict]:
params = {
"title": query,
"language.code": "en",
"per_page": 20,
"sort.by": "published_at",
"sort.order": "desc",
}
headers = {"X-API-Key": APITUBE_KEY}
async with httpx.AsyncClient(timeout=15) as client:
r = await client.get(API_URL, params=params, headers=headers)
r.raise_for_status()
return r.json().get("results", [])
def passes_filter(article: dict) -> bool:
polarity = article.get("sentiment", {}).get("overall", {}).get("polarity")
if polarity not in WANTED_POLARITY:
return False
names = {e.get("name") for e in article.get("entities", [])}
return bool(names & WATCHED_ENTITIES)
async def post_news(bot: Bot):
articles = await fetch_news("Tesla OR SpaceX OR OpenAI")
for art in articles:
if art["id"] in posted_ids or not passes_filter(art):
continue
text = (
f"<b>{html.escape(art['title'])}</b>\n"
f"{html.escape(art.get('description') or '')}\n\n"
f"{art['source']['domain']} · "
f"sentiment: {art['sentiment']['overall']['polarity']}\n"
f"<a href=\"{art['href']}\">Read more</a>"
)
try:
await bot.send_message(CHANNEL_ID, text)
posted_ids.add(art["id"])
await asyncio.sleep(1)
except TelegramRetryAfter as e:
await asyncio.sleep(e.retry_after)
except Exception:
logging.exception("send_message failed for %s", art["id"])
@dp.message(CommandStart())
async def cmd_start(m: Message):
await m.answer("News bot online. Curated negative-sentiment news on Tesla, SpaceX, OpenAI.")
@dp.message(Command("subscribe"))
async def cmd_subscribe(m: Message):
await m.answer("Posts go to the channel automatically every 15 minutes.")
@dp.message(Command("filter"))
async def cmd_filter(m: Message):
await m.answer(
f"Polarity: {sorted(WANTED_POLARITY)}\nEntities: {sorted(WATCHED_ENTITIES)}"
)
async def main():
logging.basicConfig(level=logging.INFO)
bot = Bot(TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
scheduler = AsyncIOScheduler()
scheduler.add_job(post_news, IntervalTrigger(minutes=15), args=[bot])
scheduler.start()
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Run it:
python bot.py
The first scheduler tick fires 15 minutes after start. To trigger immediately, add next_run_time=datetime.utcnow() to add_job.
Deploy with Docker
A multi-stage Python 3.12 slim image keeps the final container small and the build deterministic.
Dockerfile:
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY bot.py .
CMD ["python", "-u", "bot.py"]
requirements.txt:
aiogram==3.27.0
apscheduler==3.10.4
httpx==0.27.0
python-dotenv==1.0.1
docker-compose.yml:
services:
bot:
build: .
env_file: .env
restart: unless-stopped
For free or near-free hosting in 2026: Fly.io's hobby tier handles a single bot container fine; Railway gives you logs and one-click redeploys; or grab a $5/month VPS (Hetzner, Vultr, DigitalOcean) and run docker compose up -d. Whatever you pick, restart: unless-stopped ensures a crash doesn't take the bot offline overnight.
FAQ
How do I make a Telegram news bot in Python?
Register a bot with @BotFather, install aiogram 3.27 and a news client like httpx, fetch articles from a news API such as APITube, filter the response, and post each item to a Telegram chat or channel using bot.send_message. Wrap the fetch-and-post step in APScheduler to make it run on an interval.
Which library is better, aiogram or python-telegram-bot?
Unlike python-telegram-bot, aiogram is async-only since v3, which means it pairs cleanly with AsyncIOScheduler for channel auto-posting in a single event loop. For news/channel bots above ~50 concurrent users, pick aiogram 3.27. For command-driven personal bots, sync-codebase teams, or gradual migration paths, python-telegram-bot 21.x remains a solid choice. Both are actively maintained in 2026.
Can a Telegram bot post to a channel automatically?
Yes. Add the bot to the channel as an administrator with the "Post messages" permission, grab the channel's numeric chat ID, and call bot.send_message(chat_id, text) from a scheduled task. APScheduler's AsyncIOScheduler runs inside the bot's event loop, so one process handles both polling and scheduled posting.
Is python-telegram-bot still maintained in 2026?
Yes — python-telegram-bot is still actively maintained. Version 21.x ships an async API and ongoing fixes; the project sees regular releases on GitHub. The "is it maintained" question gets asked because the v13 sync API is deprecated, but v20 onwards is async and current.
Try it
Clone the snippets above, drop them in a folder, fill .env, docker compose up -d. APITube's free tier returns sentiment and entities on every article — get a key at apitube.io and replace the placeholder in step 3. If you want to filter on category instead of entities, read article["categories"] and match on each category's name (e.g. keep only articles whose category name contains "technology").
