
Telegram
- 51 installs
- 22 repo stars
- Updated August 1, 2026
- itechmeat/llm-code
Build Telegram bots with aiogram 3: handlers, middlewares, FSM, webhooks, keyboards, inline mode, Mini Apps and Stars payments.
About
A router skill for Telegram bot development covering aiogram 3 patterns, webhook setup, keyboard UX, inline mode, Mini Apps and Telegram Stars payments. Use it when building or modifying bots, wiring webhooks, or integrating payments and authentication.
- Task-to-reference navigation plus a definition-of-done for webhook secret validation and idempotency
- Prohibitions: no simultaneous polling+webhooks, no hardcoded tokens, always answer callback queries
Telegram by the numbers
- 51 all-time installs (skills.sh)
- Ranked #3,229 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/itechmeat/llm-code --skill telegramAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 51 |
|---|---|
| repo stars | ★ 22 |
| Last updated | August 1, 2026 |
| Repository | itechmeat/llm-code ↗ |
What it does
Build Telegram bots with aiogram 3: handlers, middlewares, FSM, webhooks, keyboards, inline mode, Mini Apps and Stars payments.
Files
Telegram (Skill Router)
Router skill: pick the reference that matches your task.
Quick Navigation
| Task | Reference |
|---|---|
| New to Telegram bots | bots-overview.md |
| Bot capabilities overview | bot-features.md |
| API methods/types | bot-api.md |
| Webhook setup & security | webhooks.md |
| aiogram 3 handlers/FSM | aiogram-patterns.md |
| Keyboard UX | keyboard-design.md |
| Inline mode | inline-mode.md |
| Mini Apps (Web Apps) | mini-apps.md |
| Payments (Stars) | payments.md |
| Authentication (Login Widget, URL Auth) | authentication.md |
| Rate limits & performance | performance.md |
Critical Prohibitions
- ❌ No polling + webhooks simultaneously for same bot
- ❌ No hardcoded tokens/secrets — use environment variables
- ❌ No secrets in callback_data or logs
- ❌ No ignoring
answer_callback_query— always respond - ❌ No blocking work in webhook handlers — use background tasks
- ❌ No trusting Login Widget data without hash verification
Definition of Done
- [ ] Webhook handlers validate
X-Telegram-Bot-Api-Secret-Token - [ ] Keyboards: max 2 buttons per row, mobile-first
- [ ] Callback data validated, not trusted blindly
- [ ] Handlers are idempotent or have de-duplication
Release Note (3.28.x / Bot API 10.0)
- aiogram
3.28.xadds Bot API10.0support, bringing guest-mode updates, richer poll/media flows, live photos, reaction-management methods, and managed/business-bot access settings. - The
3.28.1+patch line also fixesInputPollOption.mediavalidation, so poll/media integrations should assume the newer schema and the patched aiogram serializer behavior.
Links
Related Skills
- PostgreSQL — for database layer
- FastAPI — for API layer (if exists)
aiogram 3 Patterns
Async framework for Telegram bots. Python 3.9+.
Router & Handler Organization
from aiogram import Router, Bot, Dispatcher
from aiogram.types import Message
from aiogram.filters import Command
router = Router(name="main")
@router.message(Command("start"))
async def cmd_start(message: Message) -> None:
await message.answer("Hello!")
@router.message(Command("help"))
async def cmd_help(message: Message) -> None:
await message.answer("Help text...")Nested Routers
# main.py
dp = Dispatcher()
dp.include_router(main_router)
dp.include_router(admin_router)
# admin_router.py
admin_router = Router(name="admin")
admin_router.message.filter(IsAdmin()) # Filter for all handlers in routerHandler Order
Handlers are checked in registration order. First match wins.
@router.message(F.text == "specific") # First: exact match
async def handle_specific(message: Message): ...
@router.message(F.text) # Last: catch-all
async def handle_text(message: Message): ...CommandStart deep-link guard (v3.27.0)
CommandStart(deep_link=False)now rejects deep-link arguments instead of silently accepting them.- Use this strict mode for bots that want a plain
/startentry point, and register a separate deep-link-enabled handler when referral or onboarding arguments are expected.
Callback Query Handlers
from aiogram.types import CallbackQuery
@router.callback_query(F.data == "confirm")
async def on_confirm(callback: CallbackQuery) -> None:
await callback.answer("Confirmed!") # Always answer to remove loading
await callback.message.edit_text("Done")
@router.callback_query(F.data.startswith("item:"))
async def on_item(callback: CallbackQuery) -> None:
item_id = callback.data.split(":")[1]
await callback.answer()
# Process item...Critical: Always call callback.answer() to dismiss the loading indicator.
Middlewares
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from typing import Callable, Dict, Any, Awaitable
class DatabaseMiddleware(BaseMiddleware):
"""Provides database session to handlers."""
def __init__(self, session_maker):
self.session_maker = session_maker
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
async with self.session_maker() as session:
data["db_session"] = session
return await handler(event, data)
# Register
router.message.middleware(DatabaseMiddleware(session_maker))Scene transitions: middleware data is preserved (v3.26.0)
aiogram v3.26.0 preserves middleware-provided data across scene transitions. Treat it as in-memory request context:
- OK: db sessions, request-scoped services, user context
- Not OK: long-lived state (use FSM storage/DB instead)
FSM (Finite State Machine)
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
class OrderForm(StatesGroup):
waiting_for_name = State()
waiting_for_phone = State()
confirm = State()
@router.message(Command("order"))
async def start_order(message: Message, state: FSMContext) -> None:
await state.set_state(OrderForm.waiting_for_name)
await message.answer("Enter your name:")
@router.message(OrderForm.waiting_for_name)
async def process_name(message: Message, state: FSMContext) -> None:
await state.update_data(name=message.text)
await state.set_state(OrderForm.waiting_for_phone)
await message.answer("Enter phone:")
@router.message(OrderForm.waiting_for_phone)
async def process_phone(message: Message, state: FSMContext) -> None:
data = await state.get_data()
await state.clear()
await message.answer(f"Order from {data['name']}, phone: {message.text}")FSM Storage
from aiogram.fsm.storage.redis import RedisStorage
from aiogram.fsm.storage.memory import MemoryStorage
# Production: Redis
storage = RedisStorage.from_url("redis://localhost:6379/0")
# Development: Memory (lost on restart)
storage = MemoryStorage()
dp = Dispatcher(storage=storage)Filters
from aiogram.filters import Filter
from aiogram.types import Message
class IsAdmin(Filter):
def __init__(self, admin_ids: list[int]):
self.admin_ids = admin_ids
async def __call__(self, message: Message) -> bool:
return message.from_user.id in self.admin_ids
# Usage
@router.message(Command("ban"), IsAdmin([123456789]))
async def ban_user(message: Message): ...Magic Filters
from aiogram import F
@router.message(F.text.lower().contains("hello"))
async def greet(message: Message): ...
@router.message(F.photo)
async def handle_photo(message: Message): ...
@router.callback_query(F.data.in_({"yes", "no"}))
async def handle_choice(callback: CallbackQuery): ...Error Handling
from aiogram.types import ErrorEvent
@router.error()
async def error_handler(event: ErrorEvent) -> None:
logger.error(
"Exception in handler",
exc_info=event.exception,
extra={
"update_id": event.update.update_id if event.update else None,
}
)
# Optionally notify userDependency Injection
from aiogram import Bot
@router.message(Command("info"))
async def cmd_info(
message: Message,
bot: Bot, # Injected automatically
db_session: AsyncSession, # From middleware
) -> None:
me = await bot.get_me()
await message.answer(f"I am {me.username}")Logging Context
Always include in logs:
chat_id— conversation contextuser_id— Telegram userupdate_id— for tracing
import structlog
logger = structlog.get_logger()
@router.message()
async def handle(message: Message) -> None:
logger.info(
"message_received",
chat_id=message.chat.id,
user_id=message.from_user.id,
)Critical Rules
- ❌ No sync I/O in handlers — use
aiofiles,httpx, async DB - ❌ No bare
except:— catch specific exceptions - ✅ Always
callback.answer()for callback queries - ✅ Use routers for handler organization
Bot API 9.6 sync notes
- Plan for managed-bot update payloads if your bot participates in bot-management flows.
- Poll-related handlers should move to
correct_option_idsand persistent option IDs instead of assuming a single correct option and stable positional identity.
Telegram Authentication
This reference covers all Telegram authentication methods for web applications.
Methods Overview
| Method | Context | Use Case |
|---|---|---|
| Login Widget | Browser | Web admin panel, account linking |
| URL Authorization | Telegram app | Inline button → external site login |
| Mini App initData | Mini App | In-app authentication |
Login Widget
Embed on your website to let users log in via Telegram.
Setup
1. Create bot via @BotFather 2. Link domain: /setdomain → your domain 3. Add widget script:
<script async src="https://telegram.org/js/telegram-widget.js?22"
data-telegram-login="YourBotName"
data-size="large"
data-onauth="onTelegramAuth(user)"
data-request-access="write">
</script>Data Structure
class TelegramAuthData:
id: int # Telegram user ID
first_name: str # Required
last_name: str | None
username: str | None
photo_url: str | None
auth_date: int # Unix timestamp
hash: str # HMAC-SHA-256 signatureURL Authorization (Inline Buttons)
For login via inline keyboard buttons inside Telegram.
from aiogram.types import InlineKeyboardButton, LoginUrl
button = InlineKeyboardButton(
text="Log in",
login_url=LoginUrl(
url="https://example.com/auth/telegram",
request_write_access=True,
)
)User sees confirmation prompt, then redirected with auth data in query string.
Hash Verification (Critical)
Always verify hash server-side before trusting data.
Algorithm
1. Build data_check_string: sorted key=value pairs, joined by \n 2. Compute secret_key = SHA256(bot_token) 3. Compute HMAC_SHA256(data_check_string, secret_key) 4. Compare with received hash
Python Implementation
import hashlib
import hmac
from typing import Any
def verify_telegram_auth(data: dict[str, Any], bot_token: str) -> bool:
"""Verify Telegram authentication data (Login Widget / URL Auth)."""
received_hash = data.pop("hash", None)
if not received_hash:
return False
data_check_string = "\n".join(
f"{k}={v}" for k, v in sorted(data.items()) if v is not None
)
secret_key = hashlib.sha256(bot_token.encode()).digest()
computed_hash = hmac.new(
secret_key,
data_check_string.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed_hash, received_hash)Auth Date Validation
Reject stale authentications (recommended max: 5 minutes):
from datetime import datetime, timedelta, UTC
MAX_AUTH_AGE = timedelta(minutes=5)
def is_auth_fresh(auth_date: int) -> bool:
auth_time = datetime.fromtimestamp(auth_date, tz=UTC)
return datetime.now(UTC) - auth_time < MAX_AUTH_AGEMini App Authentication
Different algorithm — uses "WebAppData" as HMAC key prefix.
def validate_mini_app_init_data(init_data: str, bot_token: str) -> bool:
"""Validate Mini App initData."""
from urllib.parse import parse_qsl
parsed = dict(parse_qsl(init_data, keep_blank_values=True))
received_hash = parsed.pop('hash', None)
if not received_hash:
return False
data_check_string = '\n'.join(
f'{k}={v}' for k, v in sorted(parsed.items())
)
# Different from Login Widget: HMAC with "WebAppData" prefix
secret_key = hmac.new(
b'WebAppData',
bot_token.encode(),
hashlib.sha256
).digest()
calculated_hash = hmac.new(
secret_key,
data_check_string.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(calculated_hash, received_hash)FastAPI Endpoint Example
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter()
class TelegramAuthData(BaseModel):
id: int
first_name: str
last_name: str | None = None
username: str | None = None
photo_url: str | None = None
auth_date: int
hash: str
@router.post("/auth/telegram")
async def telegram_login(data: TelegramAuthData):
if not verify_telegram_auth(data.model_dump(), settings.BOT_TOKEN):
raise HTTPException(401, "Invalid auth")
if not is_auth_fresh(data.auth_date):
raise HTTPException(401, "Auth expired")
account = await account_service.find_or_create_by_telegram(
telegram_id=data.id,
first_name=data.first_name,
)
return {"token": await create_session(account.id)}Security Checklist
- ✅ Always verify hash server-side
- ✅ Check
auth_datefreshness (5 minutes max) - ✅ Use HTTPS for callback URLs
- ✅ Store
bot_tokenin environment variables - ✅ Use
hmac.compare_digest()for constant-time comparison - ✅ Link domains in @BotFather before use
See Also
- mini-apps.md — Full Mini App documentation
- keyboard-design.md — Inline keyboard patterns
Telegram Bot API Reference
Overview
The Bot API is an HTTP-based interface for building Telegram bots. It provides:
- HTTPS requests to
https://api.telegram.org/bot<token>/METHOD_NAME - JSON responses
- Webhook or long polling for updates
Bot API 10.0 is supported in aiogram v3.28.x.
Bot API 10.0 highlights
- Guest mode: new guest query/message payloads let bots reply in some chats even without membership.
- Polls: media in poll questions/options/explanations,
members_only,country_codes, and one-option poll flows. - Media: live photos are now sendable/editable and can appear in paid media and media groups.
- Chat management: reaction-deletion methods and
can_react_to_messagespermissions. - Managed/business bots: access-settings methods plus broader bot-to-bot and business-account flows.
Bot API 9.6 highlights
- Managed bots: new managed-bot request flows, token-management methods, and update payloads.
- Polls: multiple correct answers, revoting, option shuffling/addition, descriptions, and persistent option IDs.
- Formatting:
date_timeentities are allowed in more quote/checklist/gift contexts.
Authentication
All requests require bot token:
https://api.telegram.org/bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/getMeCritical: Never expose token in client code or logs.
Getting Updates
Polling (getUpdates)
# Not recommended for production
response = await client.get(
f"https://api.telegram.org/bot{token}/getUpdates",
params={"offset": last_update_id + 1, "timeout": 30}
)Webhooks (recommended)
# Set webhook
await client.post(
f"https://api.telegram.org/bot{token}/setWebhook",
json={
"url": "https://example.com/webhook/secret123",
"secret_token": "my-secret-token", # For verification
"allowed_updates": ["message", "callback_query"],
}
)Core Methods
Messages
| Method | Description |
|---|---|
sendMessage | Send text message |
sendPhoto | Send photo |
sendDocument | Send file |
sendLocation | Send location |
sendVenue | Send venue |
sendContact | Send contact |
sendPoll | Send poll |
sendLivePhoto | Send live photo |
sendDice | Send dice/emoji animation |
editMessageText | Edit message text |
editMessageReplyMarkup | Edit inline keyboard |
deleteMessage | Delete message |
forwardMessage | Forward message |
copyMessage | Copy message |
Chats
| Method | Description |
|---|---|
getChat | Get chat info |
getChatMember | Get member info |
getChatMemberCount | Get member count |
getChatAdministrators | List admins |
deleteAllMessageReactions | Delete all reactions |
deleteMessageReaction | Delete one reaction |
banChatMember | Ban user |
unbanChatMember | Unban user |
restrictChatMember | Restrict permissions |
promoteChatMember | Promote to admin |
setChatTitle | Set chat title |
setChatDescription | Set description |
pinChatMessage | Pin message |
leaveChat | Leave chat |
Bot Info
| Method | Description |
|---|---|
getMe | Get bot info |
setMyCommands | Set bot commands |
getMyCommands | Get bot commands |
setMyDescription | Set bot description |
setMyShortDescription | Set short description |
getManagedBotToken | Get token for managed bot |
replaceManagedBotToken | Rotate managed bot token |
getManagedBotAccessSettings | Read managed-bot access settings |
setManagedBotAccessSettings | Update managed-bot access settings |
Mini Apps / prepared buttons
| Method | Description |
|---|---|
savePreparedKeyboardButton | Save reusable keyboard request button |
Inline Mode
| Method | Description |
|---|---|
answerInlineQuery | Answer inline query |
answerWebAppQuery | Answer web app query |
Guest queries
| Method | Description |
|---|---|
answerGuestQuery | Answer a guest-mode query |
Callbacks
| Method | Description |
|---|---|
answerCallbackQuery | Answer button callback |
Payments
| Method | Description |
|---|---|
sendInvoice | Send payment invoice |
answerPreCheckoutQuery | Confirm checkout |
answerShippingQuery | Answer shipping |
createInvoiceLink | Create invoice link |
refundStarPayment | Refund Stars |
Update Types
class Update:
update_id: int
message: Message | None
edited_message: Message | None
channel_post: Message | None
edited_channel_post: Message | None
callback_query: CallbackQuery | None
inline_query: InlineQuery | None
chosen_inline_result: ChosenInlineResult | None
shipping_query: ShippingQuery | None
pre_checkout_query: PreCheckoutQuery | None
poll: Poll | None
poll_answer: PollAnswer | None
my_chat_member: ChatMemberUpdated | None
chat_member: ChatMemberUpdated | None
chat_join_request: ChatJoinRequest | None
managed_bot: ManagedBotUpdated | None
guest_message: Message | NoneCommon Types
Message
class Message:
message_id: int
date: int
chat: Chat
from_user: User | None
text: str | None
entities: list[MessageEntity] | None
reply_to_message: Message | None
# ... many more fieldsBot API 10.0 adds guest-mode caller metadata (guest_query_id, guest_bot_caller_user, guest_bot_caller_chat) and live_photo; Bot API 9.6 also added reply_to_poll_option_id for reply flows anchored to a poll option.
Chat
class Chat:
id: int
type: str # "private", "group", "supergroup", "channel"
title: str | None
username: str | NoneUser
class User:
id: int
is_bot: bool
first_name: str
last_name: str | None
username: str | None
language_code: str | None
can_manage_bots: bool | None
supports_guest_queries: bool | NonePoll (Bot API 9.6-10.0)
class Poll:
id: str
question: str
options: list[PollOption]
allows_multiple_answers: bool
correct_option_ids: list[int] | None
allows_revoting: bool | None
description: str | None
media: PollMedia | None
explanation_media: PollMedia | None
members_only: bool | None
country_codes: list[str] | NoneUse correct_option_ids instead of the old single-answer mental model, do not key business logic only by option position when persistent option IDs are available, and update validators/serializers for media-bearing poll options on aiogram 3.28.1+.
Contact
class Contact:
phone_number: str
first_name: str
last_name: str | None
user_id: int | None
vcard: str | None
full_name: str | None # Bot API 9.4CallbackQuery
class CallbackQuery:
id: str
from_user: User
message: Message | None
inline_message_id: str | None
chat_instance: str
data: str | None # max 64 bytesError Handling
Bot API returns:
{
"ok": false,
"error_code": 400,
"description": "Bad Request: message text is empty"
}Common error codes:
400— Bad request (invalid parameters)401— Unauthorized (invalid token)403— Forbidden (bot blocked by user)404— Not found429— Too many requests (rate limited)
Rate Limits
- ~30 messages/second to different chats
- ~1 message/second to same chat
- ~20 messages/minute to same group
- Bulk limits for notifications
On 429 error, check retry_after in response.
Formatting
MarkdownV2
``` *bold* _italic_ __underline__ ~strikethrough~ ||spoiler|| inline code `pre` [link](https://example.com) ```
Escape special characters: _*[]()~>#+-=|{}.!\`
HTML
<b>bold</b>
<i>italic</i>
<u>underline</u>
<s>strikethrough</s>
<tg-spoiler>spoiler</tg-spoiler>
<code>inline code</code>
<pre>code block</pre>
<a href="https://example.com">link</a>Links
- Full API docs: https://core.telegram.org/bots/api
- Changelog: https://core.telegram.org/bots/api-changelog
- @BotNews — API updates channel
Telegram Bot Features Overview
Quick reference of what bots can do. For details, see specialized references.
Commands
- Start with
/, up to 32 chars, lowercase recommended - Standard:
/start,/help,/settings - Configure via @BotFather or
setMyCommandsAPI - Scope by chat type, user role, language
See also: bots-overview.md
Keyboards
| Type | Purpose |
|---|---|
| Reply Keyboard | Custom keyboard below input field |
| Inline Keyboard | Buttons attached to messages |
Button types: callback, URL, switch-inline, login_url, web_app, pay.
Bots can now set button colors and emoji to emphasize primary actions.
See also: keyboard-design.md
Inline Mode
Users query bot from any chat: @YourBot query
Enable in @BotFather → /setinline
See also: inline-mode.md
Payments
| Mode | Currency | Use Case |
|---|---|---|
| Physical goods | Fiat (USD, EUR, etc.) | Shipping required |
| Digital goods | Telegram Stars (XTR) | Subscriptions, in-app |
See also: payments.md
Managed & Business Bots (Bot API 9.6-10.0)
- Telegram now supports managed-bot flows where one bot can help create and operate another bot.
- New button/request flows allow asking the user for a managed bot directly from keyboards and Mini Apps.
- Managed bot creation and token-change events are now first-class update/message payloads, so webhook routing and allowlist logic should treat them as operationally sensitive events.
- Bot API 10.0 adds managed-bot access settings methods and lets business bots manage user accounts without requiring Telegram Premium.
- If bot-to-bot communication is enabled, treat conversations with other bots as explicit allowlisted flows rather than assuming all peer bots are unreachable.
Guest Mode (Bot API 10.0)
- Bots can now receive certain guest-mode messages and reply in chats where they are not members.
- New guest-mode payloads (
guest_message,guest_query_id, caller user/chat metadata) should be routed separately from normal membership-based chat flows. - Do not assume chat membership when authorizing guest-mode replies, logging sender context, or deciding whether a bot may answer.
Polls (Bot API 9.6-10.0)
- Quizzes can now have multiple correct answers.
- Polls can allow revoting, shuffle options, hide results until close, and carry a description.
- Option metadata now has persistent identifiers and audit-style fields for who added an option and when.
- Bot API 10.0 adds media in poll questions/options/explanations,
members_only,country_codes, and allows single-option poll flows. - If your bot stores poll state externally, prefer persistent option IDs over positional assumptions, and validate media-bearing options with the patched aiogram
3.28.1+serializers.
Media Updates (Bot API 10.0)
- Live photos are now first-class media with dedicated send/edit/media-group support.
- Polls can carry media in the question, answer options, and quiz explanations.
- If your bot mirrors Telegram media types in a strict schema, add live-photo and poll-media variants before enforcing validation.
Chat Management (Bot API 10.0)
- Bots can now delete reactions from messages and work with
can_react_to_messagespermissions. getChatAdministratorscan optionally return bots, which matters if your admin sync logic previously filtered them out by assumption.
Mini Apps
JavaScript web apps inside Telegram with native features:
- Theme integration
- Haptic feedback
- Cloud storage
- QR scanner
- Fullscreen mode
requestChatfromWebApp- Prepared keyboard buttons for requesting users, chats, and managed bots
See also: mini-apps.md
HTML5 Games
1. Create via @BotFather /newgame 2. Send via sendGame 3. Track scores with setGameScore
Rate Limits
| Tier | Rate | Cost |
|---|---|---|
| Standard | 30 msg/sec | Free |
| Increased | 1000 msg/sec | 0.1 Star/msg |
Monetization Options
- Digital product sales (Telegram Stars)
- Paid media (photos/videos)
- Star subscriptions
- Star reactions (channels)
- Affiliate programs
- Telegram Ads revenue share (50%)
Platform Updates (2025-2026)
These are client/platform features (not direct Bot API controls), but they affect user expectations:
- AI summaries for channel posts and Instant View pages.
- Collectible gifts: crafting system and gift marketplace.
- Gift purchase offers using Stars or TON.
- Passkeys for secure Telegram logins.
Links
- Commands: https://core.telegram.org/bots/features#commands
- Keyboards: https://core.telegram.org/bots/features#keyboards
- Inline Mode: https://core.telegram.org/bots/inline
- Payments: https://core.telegram.org/bots/payments
- Mini Apps: https://core.telegram.org/bots/webapps
Telegram Bots Overview
What Bots Can Do
Telegram bots are small applications that run entirely within the Telegram app. Key capabilities:
- Replace websites: Host Mini Apps built with JavaScript
- AI chatbots: Native support for threaded conversations and streaming responses
- Business integration: Process messages on behalf of business accounts
- Payments: Sell digital products via Telegram Stars, physical via third-party providers
- Custom tools: File conversion, chat management, utilities
- Games: HTML5 games with leaderboards
- Inline mode: Search and share content from any chat
How Bots Differ from Users
| Aspect | User | Bot |
|---|---|---|
| Status | "last seen" / "online" | "bot" label |
| Cloud storage | Full history | Older messages may be removed |
| Starting conversations | Can message anyone | User must send first message or add to group |
| Group visibility | All messages | Only relevant messages (privacy mode) |
| Phone number | Required | Not needed |
Bot Links
- Standard format:
@YourBot(requires 'bot' suffix) - Short link:
t.me/YourBot - Collectible usernames: Can omit 'bot' suffix (e.g., @stickers, @gif)
Creating a Bot
1. Message @BotFather on Telegram 2. Use /newbot command 3. Choose a name and username 4. Receive authentication token
Critical: Store bot token securely. Anyone with the token has full control.
Global Commands
All bots should support these commands:
| Command | Purpose |
|---|---|
/start | Begin interaction, can pass deep-linking parameters |
/help | Return help message with bot description |
/settings | Show/edit bot settings (if applicable) |
Command Scopes
Bots can show different commands to different users:
- Based on chat type (private, group, channel)
- Based on user role (admin, member)
- Based on user's
language_code
Important: Always validate commands server-side regardless of scope.
Privacy Mode
Default behavior in groups:
Bots receive:
- Commands explicitly for them (
/command@this_bot) - General commands if bot was last to message
- Inline messages sent via the bot
- Replies to bot's messages
- All service messages
Bots don't receive:
- Regular messages not addressed to them
Exceptions:
- Admin bots receive all messages
- Privacy mode can be disabled in @BotFather
Admin Rights
Bot admins always receive all messages in groups. Consider requesting only needed permissions:
can_delete_messages— for moderation botscan_restrict_members— for anti-spam botscan_pin_messages— for announcement botscan_manage_chat— for general management
Deep Linking
Pass parameters via /start:
https://t.me/YourBot?start=payload123Bot receives: /start payload123
Use for:
- Referral tracking
- Feature activation
- User onboarding flows
Inline Mode
Overview
Inline mode allows users to call your bot from any chat by typing @YourBot query in the message input field. Results appear instantly without sending messages.
Enable inline mode: Send /setinline to @BotFather and provide placeholder text.
How It Works
1. User types @YourBot query in any chat 2. Bot receives InlineQuery update 3. Bot returns results via answerInlineQuery 4. User selects result, it's sent to current chat 5. Bot receives ChosenInlineResult if feedback is enabled
Supported Result Types (20+)
InlineQueryResultArticle— generic text/mediaInlineQueryResultPhoto— photosInlineQueryResultGif— GIFsInlineQueryResultMpeg4Gif— animated GIFs (MPEG4)InlineQueryResultVideo— videosInlineQueryResultAudio— audio filesInlineQueryResultVoice— voice messagesInlineQueryResultDocument— documentsInlineQueryResultLocation— locationsInlineQueryResultVenue— venuesInlineQueryResultContact— contactsInlineQueryResultSticker— stickersInlineQueryResultCachedPhoto— cached photos- And more...
Basic Implementation (aiogram 3)
from aiogram import Router
from aiogram.types import (
InlineQuery,
InlineQueryResultArticle,
InputTextMessageContent,
)
router = Router()
@router.inline_query()
async def inline_handler(query: InlineQuery):
results = []
# Search based on query.query
search_text = query.query or ""
# Build results
results.append(
InlineQueryResultArticle(
id="1",
title="Result Title",
description="Result description",
input_message_content=InputTextMessageContent(
message_text=f"You searched: {search_text}"
),
)
)
await query.answer(
results=results,
cache_time=300, # Cache results for 5 minutes
is_personal=True, # Results specific to this user
)Switch to PM (Private Message)
For bots that need user setup (auth, linking accounts):
await query.answer(
results=[],
switch_pm_text="Sign in to continue",
switch_pm_parameter="inline_auth", # Received in /start
)User clicks button → opens PM with bot → /start inline_auth received.
After setup, return user to original chat:
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text="Return to chat",
switch_inline_query="" # Opens inline query in original chat
)]
])Location-Based Results
Enable with /setinlinegeo in @BotFather.
@router.inline_query()
async def inline_location_handler(query: InlineQuery):
if query.location:
lat = query.location.latitude
lon = query.location.longitude
# Use location for nearby resultsUser must grant location permission to bot.
Feedback Collection
Enable with /setinlinefeedback in @BotFather.
from aiogram.types import ChosenInlineResult
@router.chosen_inline_result()
async def chosen_result_handler(chosen: ChosenInlineResult):
result_id = chosen.result_id
user_id = chosen.from_user.id
query = chosen.query
# Track which results users selectWarning: High-traffic bots may receive more feedback than actual requests due to caching. Adjust probability setting in @BotFather.
Caching
cache_time parameter in answerInlineQuery:
| Value | Effect |
|---|---|
| 0 | No caching (every keystroke = new request) |
| 300 | 5 minutes (recommended default) |
| Higher | Less load, but stale results |
is_personal:
True— cache per userFalse— cache shared across users (for generic results)
Best Practices
1. Fast response: Users expect instant results 2. Meaningful previews: Good titles and descriptions 3. Limit results: 10-50 results max, paginate if needed 4. Cache appropriately: Balance freshness vs. load 5. Handle empty query: Show popular/default results
Viral Spreading
Messages sent via inline show bot username next to sender. Users can tap it to use inline query themselves.
Example Bots
- @gif — GIF search
- @pic — Image search
- @wiki — Wikipedia search
- @bold — Text formatting
- @youtube — Video search (with auth)
- @foursquare — Location-based venues
Links
- Official docs: https://core.telegram.org/bots/inline
- Bot API: https://core.telegram.org/bots/api#inline-mode
Keyboard Design
Principles
- Progressive disclosure: show 3–5 relevant actions, not 20.
- Mobile-first: max 2 buttons per row; avoid long labels.
- Navigation consistency: always provide "Back"/"Main menu" for multi-step flows.
- Responsiveness: always
answer_callback_queryquickly to clear the loading spinner. - Emphasis: use button colors/emoji to highlight primary actions when available.
- aiogram (v3.26.0+): keyboard builder button helpers add
icon/styleparams; use them for UX polish, not for critical meaning.
Callback data design
Use short, parseable callback data strings:
- Prefix by feature:
evt:/place:/page: - Include only identifiers you can validate (never trust arbitrary client data)
Example conventions
place:details:<place_id>place:save:<place_id>page:<page_number>
Callback data limits
Telegram limits callback_data to 64 bytes. Keep it short and parseable.
Common patterns
Action keyboard (inline)
- Primary action(s) on the first row
- Secondary action(s) below
- Always include a navigation row (Back/Main)
Pagination
- Show
Prev/Nextwith a page indicator - Use a no-op callback for the page indicator
Edit vs reply
- Prefer editing the existing message during navigation flows to avoid chat spam.
- Prefer replying with a new message when:
- the content is important for history (receipts, confirmations)
- the edited message is too old or already removed
Safety checklist
- Always
answer_callback_query. - Validate identifiers from callback data (types, ownership/tenant).
- Never include secrets or tokens in callback data.
- Keep callback data within Telegram limits (64 bytes).
Telegram Mini Apps
Interactive web apps inside Telegram with native features.
Initialization
<script src="https://telegram.org/js/telegram-web-app.js"></script>const tg = window.Telegram.WebApp;
tg.ready(); // Signal app is ready
tg.expand(); // Expand to full heightCore Properties
| Property | Description |
|---|---|
initData | Raw data for server validation |
initDataUnsafe | Parsed data (DO NOT trust server-side) |
platform | ios, android, tdesktop, etc. |
colorScheme | "light" or "dark" |
themeParams | Current theme colors |
viewportHeight | Visible area height |
isFullscreen | Fullscreen mode (Bot API 8.0+) |
Theme CSS Variables
.container {
background: var(--tg-theme-bg-color);
color: var(--tg-theme-text-color);
}
.button {
background: var(--tg-theme-button-color);
color: var(--tg-theme-button-text-color);
}Available: --tg-theme-bg-color, --tg-theme-text-color, --tg-theme-hint-color, --tg-theme-link-color, --tg-theme-button-color, --tg-theme-button-text-color, --tg-theme-secondary-bg-color, --tg-theme-header-bg-color.
Main Button
tg.MainButton
.setText('Submit')
.show()
.onClick(() => {
tg.MainButton.showProgress();
// Submit logic
tg.MainButton.hideProgress();
});Back Button
tg.BackButton.show();
tg.BackButton.onClick(() => tg.BackButton.hide());Haptic Feedback
tg.HapticFeedback.impactOccurred('medium'); // light|medium|heavy|rigid|soft
tg.HapticFeedback.notificationOccurred('success'); // success|error|warningCloud Storage
Up to 1024 items per user. Key: 1-128 chars, Value: 0-4096 chars.
tg.CloudStorage.setItem('key', 'value');
tg.CloudStorage.getItem('key', (err, val) => { /* ... */ });
tg.CloudStorage.removeItem('key');Dialogs
tg.showAlert('Done!');
tg.showConfirm('Delete?', (ok) => { if (ok) { /* ... */ } });
tg.showPopup({
title: 'Action',
message: 'Choose:',
buttons: [
{ id: 'yes', type: 'default', text: 'Yes' },
{ id: 'no', type: 'cancel' }
]
}, (id) => { /* ... */ });QR Scanner
tg.showScanQrPopup({ text: 'Scan code' }, (data) => {
if (data) { tg.closeScanQrPopup(); return true; }
});Events
tg.onEvent('themeChanged', () => { /* ... */ });
tg.onEvent('viewportChanged', ({ isStateStable }) => { /* ... */ });
tg.onEvent('mainButtonClicked', () => { /* ... */ });Server-Side Validation
Always validate initData server-side.
import hashlib, hmac
from urllib.parse import parse_qsl
def validate_init_data(init_data: str, bot_token: str) -> bool:
parsed = dict(parse_qsl(init_data, keep_blank_values=True))
received_hash = parsed.pop('hash', None)
if not received_hash:
return False
data_check_string = '\n'.join(
f'{k}={v}' for k, v in sorted(parsed.items())
)
# Note: HMAC key is "WebAppData" + token, different from Login Widget
secret_key = hmac.new(b'WebAppData', bot_token.encode(), hashlib.sha256).digest()
calculated = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(calculated, received_hash)Mini Apps 2.0 (Bot API 8.0+)
| Feature | Method |
|---|---|
| Fullscreen | requestFullscreen() / exitFullscreen() |
| Lock orientation | lockOrientation() / unlockOrientation() |
| Home screen | addToHomeScreen() |
| Safe area | safeAreaInset property |
Critical Rules
- ❌ Never trust
initDataUnsafeon server — validateinitData - ❌ Never use hardcoded colors — use theme CSS variables
- ✅ Check
auth_datefreshness - ✅ Handle
viewportChangedfor responsive layout - ✅ Use
isVersionAtLeast()for feature detection
Links
- Official docs: https://core.telegram.org/bots/webapps
- Authentication: authentication.md
Telegram Payments
Reference: https://core.telegram.org/bots/payments
Overview
Telegram Payments API — open platform for accepting payments via bots. Supports physical goods (via payment providers) and digital goods (via Telegram Stars).
Two Payment Modes
| Mode | Currency | Use Case | Provider |
|---|---|---|---|
| Physical goods | 30+ fiat currencies | Products requiring shipping | Stripe, YooKassa, etc. |
| Digital goods | Telegram Stars (XTR) | Subscriptions, in-app purchases, digital content | Telegram (no external provider) |
Physical Goods: Payment Flow
1. Setup Provider Token
1. BotFather → /mybots → Select bot 2. Bot Settings → Payments 3. Choose provider (Stripe, etc.) 4. Get token (format: 123:LIVE:XXXX)
2. Send Invoice
from aiogram import Bot
from aiogram.types import LabeledPrice
async def send_invoice(bot: Bot, chat_id: int, provider_token: str):
await bot.send_invoice(
chat_id=chat_id,
title="Product Name",
description="Product description",
payload="unique_payload_id",
provider_token=provider_token,
currency="USD",
prices=[
LabeledPrice(label="Product", amount=1000), # $10.00 (in cents)
LabeledPrice(label="Shipping", amount=500), # $5.00
],
# Optional
need_name=True,
need_phone_number=True,
need_email=True,
need_shipping_address=True,
is_flexible=True, # Enable shipping options callback
start_parameter="product_123", # For deep links
max_tip_amount=1000, # Max tip $10
suggested_tip_amounts=[100, 200, 500], # Suggested tips
)3. Handle Shipping Query (if is_flexible=True)
from aiogram import Router, F
from aiogram.types import ShippingQuery, ShippingOption
router = Router()
@router.shipping_query()
async def on_shipping_query(query: ShippingQuery):
# Check if delivery is available
if query.shipping_address.country_code not in ["US", "CA"]:
await query.answer(ok=False, error_message="Delivery not available")
return
# Offer shipping options
await query.answer(
ok=True,
shipping_options=[
ShippingOption(
id="standard",
title="Standard Delivery",
prices=[LabeledPrice(label="Standard", amount=500)]
),
ShippingOption(
id="express",
title="Express Delivery",
prices=[LabeledPrice(label="Express", amount=1500)]
),
]
)4. Handle Pre-Checkout Query
from aiogram.types import PreCheckoutQuery
@router.pre_checkout_query()
async def on_pre_checkout(query: PreCheckoutQuery):
"""MUST answer within 10 seconds."""
# Verify order can be fulfilled
if not await check_inventory(query.invoice_payload):
await query.answer(
ok=False,
error_message="Sorry, this item is no longer available"
)
return
await query.answer(ok=True)5. Handle Successful Payment
from aiogram.types import Message
from aiogram import F
@router.message(F.successful_payment)
async def on_successful_payment(message: Message):
payment = message.successful_payment
# Process order
await process_order(
user_id=message.from_user.id,
payload=payment.invoice_payload,
total_amount=payment.total_amount,
currency=payment.currency,
telegram_payment_charge_id=payment.telegram_payment_charge_id,
provider_payment_charge_id=payment.provider_payment_charge_id,
# Shipping info if requested
shipping_address=payment.order_info.shipping_address if payment.order_info else None,
)
await message.answer("✅ Thank you! Your order has been placed.")Digital Goods: Telegram Stars
Send Invoice for Stars
async def send_stars_invoice(bot: Bot, chat_id: int):
await bot.send_invoice(
chat_id=chat_id,
title="Premium Subscription",
description="1 month of premium features",
payload="premium_1month",
provider_token="", # Empty for Stars
currency="XTR", # Telegram Stars
prices=[
LabeledPrice(label="Premium", amount=100), # 100 Stars
],
)Handle Stars Payment
@router.message(F.successful_payment)
async def on_stars_payment(message: Message):
payment = message.successful_payment
if payment.currency == "XTR":
# Stars payment
await grant_premium(
user_id=message.from_user.id,
stars_paid=payment.total_amount,
)Refund Stars
async def refund_stars(bot: Bot, user_id: int, charge_id: str):
"""Refund Telegram Stars payment."""
await bot.refund_star_payment(
user_id=user_id,
telegram_payment_charge_id=charge_id,
)Inline Invoices
from aiogram.types import InlineQuery, InlineQueryResultArticle, InputInvoiceMessageContent
@router.inline_query()
async def on_inline_query(query: InlineQuery):
results = [
InlineQueryResultArticle(
id="product_123",
title="Buy Premium",
description="100 Stars",
input_message_content=InputInvoiceMessageContent(
title="Premium Subscription",
description="1 month premium",
payload="inline_premium_123",
provider_token="",
currency="XTR",
prices=[LabeledPrice(label="Premium", amount=100)],
),
)
]
await query.answer(results)Paid Media (Channels/Bots)
from aiogram.types import InputMediaPhoto
async def send_paid_media(bot: Bot, chat_id: int):
"""Send paid photos/videos (Stars required to view)."""
await bot.send_paid_media(
chat_id=chat_id,
star_count=10, # Price in Stars
media=[
InputMediaPhoto(media="file_id_or_url"),
],
caption="Exclusive content! Pay 10 Stars to unlock.",
)Star Subscriptions
from aiogram.types import ChatInviteLink
async def create_paid_subscription(bot: Bot, chat_id: int):
"""Create invite link with monthly Star subscription."""
link = await bot.create_chat_subscription_invite_link(
chat_id=chat_id,
subscription_period=2592000, # 30 days in seconds
subscription_price=50, # 50 Stars/month
name="VIP Access",
)
return link.invite_linkStar Reactions (Channels)
Channel owners can enable paid Star reactions:
- Channel Settings → Reactions → Enable Paid Reactions
- Owner receives 100% of Stars
- Stars can be converted to Toncoin or ad credits
Affiliate Programs
Developers can create affiliate programs for mini apps:
# Users get referral links
# When referred users make Star purchases, referrer earns commission
# Commission % and duration set by developerTesting Payments
Stripe Test Mode
1. BotFather → Payments → Select "Stripe TEST MODE" 2. Use test card: 4242 4242 4242 4242 3. Any future expiry, any CVC
Test Token Format
- Test:
123:TEST:XXXX - Live:
123:LIVE:XXXX
Currency Amounts
Amounts in smallest currency units (cents, kopecks, etc.):
| Currency | Amount | Meaning |
|---|---|---|
| USD | 1000 | $10.00 |
| EUR | 1500 | €15.00 |
| RUB | 100000 | 1000.00 ₽ |
| XTR | 100 | 100 Stars |
Limits
- Min: ~$1 equivalent
- Max: ~$10,000 equivalent
- Stars: No external limits
Supported Currencies (30+)
USD, EUR, GBP, RUB, UAH, BYN, KZT, AED, AUD, CAD, CHF, CNY, CZK, DKK, HKD, HUF, IDR, ILS, INR, JPY, KRW, MXN, MYR, NOK, NZD, PHP, PLN, RON, SEK, SGD, THB, TRY, TWD, ZAR, BRL, ARS, CLP, COP, PEN, VND...
XTR — Telegram Stars (digital goods only)
Webhook vs Polling
Payments work with both modes, but webhooks recommended for:
- Lower latency
- Production reliability
- Required for some payment flows
Error Handling
from aiogram.exceptions import TelegramBadRequest
@router.pre_checkout_query()
async def on_pre_checkout(query: PreCheckoutQuery):
try:
await query.answer(ok=True)
except TelegramBadRequest as e:
logger.error(f"Pre-checkout failed: {e}")
# Payment flow cancelledLive Checklist
Before going live:
- [ ] Enable 2FA on bot owner account
- [ ] Implement
/termscommand with Terms & Conditions - [ ] Implement
/supportcommand or contact method - [ ] Handle disputes and chargebacks
- [ ] Backup payment records
- [ ] Complete provider's live checklist (e.g., Stripe)
- [ ] Replace TEST token with LIVE token
Monetization Summary
| Feature | Stars Required | Who Receives |
|---|---|---|
| Digital product purchase | Yes | Bot developer |
| Paid media (photos/videos) | Yes | Channel/bot owner |
| Star reactions | Yes | Channel owner (100%) |
| Star subscriptions | Yes | Channel owner |
| Affiliate commissions | User's Stars | Referrer |
Withdrawing Stars
Developers can withdraw earned Stars as: 1. Toncoin via Fragment (minimal commission) 2. Telegram Ads credits (30% bonus subsidy)
Prohibitions
- ❌ Do NOT store provider_token in code — use env variables only
- ❌ Do NOT accept payments without inventory check in pre_checkout
- ❌ Do NOT ignore shipping_query (10 sec timeout)
- ❌ Do NOT use LIVE token without completing live checklist
- ❌ Do NOT sell prohibited items (see Stripe Prohibited Businesses)
See Also
- bot-api.md — Payment methods reference
- mini-apps.md — answerWebAppQuery for Mini App payments
- inline-mode.md — Inline invoices
Performance & Rate Limits
Telegram rate limits
Telegram imposes rate limits on bot API calls:
- ~30 messages per second to different chats
- ~1 message per second to the same chat
- Editing messages: similar limits apply
Message throttling
When streaming or editing messages, throttle edits to avoid rate limits:
- Batch edits (e.g., update every 1-2 seconds, not on every token)
- Use exponential backoff on 429 errors
Webhook handler performance
- Keep webhook handling fast (< 1 second)
- Avoid heavy AI/RAG work directly in the webhook request path
- Offload to background tasks/queues
Background processing
Use task queues for:
- AI model inference
- RAG retrieval and generation
- External API calls
- Database-heavy operations
Logging and monitoring
Log failures with enough context:
bot_idchat_iduser_telegram_idupdate_id- Error details
Health checks
If Telegram-related health checks are implemented:
- Treat as external dependency checks
- Keep lightweight
- Check webhook connectivity separately from bot API
Webhooks and Update Processing
Webhook Setup
One webhook URL per bot: /webhook/{webhook_token}.
# Set webhook with secret token
await bot.set_webhook(
url=f"https://example.com/webhook/{webhook_token}",
secret_token="my-secret-token",
allowed_updates=["message", "callback_query", "inline_query"],
)Security
Validate X-Telegram-Bot-Api-Secret-Token header on every request:
from fastapi import Header, HTTPException
async def verify_telegram_secret(
x_telegram_bot_api_secret_token: str = Header(None)
):
if x_telegram_bot_api_secret_token != settings.WEBHOOK_SECRET:
raise HTTPException(status_code=403, detail="Invalid secret")Store tokens in environment variables. Never hardcode or log them.
Proxy deployments (v3.26.0 note)
If you run behind a reverse proxy / load balancer:
- Ensure
X-Telegram-Bot-Api-Secret-Tokenis forwarded end-to-end (some proxies strip unknown headers by default). - Avoid shared webhook paths across bots unless the
{webhook_token}is unique and validated. - Prefer terminating TLS at the edge, but keep an allowlist for internal hops and avoid re-exposing the webhook to the public internet.
Polling vs Webhooks
| Aspect | Polling | Webhooks |
|---|---|---|
| Production | ❌ Not recommended | ✅ Recommended |
| Setup | Simpler | Requires HTTPS endpoint |
| Latency | Higher (polling interval) | Instant |
| Reliability | Less reliable | More reliable |
Critical: Never run polling and webhooks simultaneously for the same bot.
Update Processing
At-Least-Once Delivery
Telegram may retry webhook delivery on timeout/error:
- Make handlers idempotent
- Or implement de-duplication (store last
update_idper bot)
Fast Acknowledgement
@router.message()
async def handle_message(message: Message):
# Return 200 OK quickly to Telegram
# Offload heavy work to background
background_tasks.add_task(process_ai_response, message)
return {"ok": True}Keep webhook handling under 1 second. Offload AI/RAG work to task queues.
Update Types
| Type | Use Case |
|---|---|
message | Text, commands, media, service messages |
callback_query | Inline keyboard button presses |
inline_query | Inline mode queries |
pre_checkout_query | Payment confirmation |
shipping_query | Shipping options request |
Error Handling
@router.message()
async def handle_message(message: Message):
try:
await process_message(message)
except Exception as e:
logger.error(
"Message processing failed",
extra={
"bot_id": bot_id,
"chat_id": message.chat.id,
"update_id": update_id,
"error": str(e),
}
)
# Return 200 OK to prevent Telegram retries
return {"ok": True}Rate Limits
- ~30 messages/second to different chats
- ~1 message/second to same chat
- ~20 messages/minute to same group
On 429 error, respect retry_after. Use exponential backoff.
Logging Context
Always include:
bot_idchat_iduser_telegram_idupdate_id