Building a Telegram bot in Python, from token to production
To build a Telegram bot in Python, get a token from @BotFather with /newbot and send HTTPS requests to the Bot API. Updates arrive in one of two ways: getUpdates (long polling) or a webhook — never both at once. In production, protect the webhook with secret_token, stay within the documented limits and wait retry_after seconds on a 429.
Updated:
How does a Telegram bot work?
A bot is a special Telegram account driven by your code. Every action is a plain HTTPS request to https://api.telegram.org/bot<token>/METHOD_NAME. When someone messages the bot, Telegram stores the event as an update: you fetch it with getUpdates, or Telegram POSTs it to your webhook. Replying is just another request, such as sendMessage.
Bots differ from users in important ways: a bot cannot start a conversation — a person has to message it first or add it to a group. In groups, a bot sees only messages meant for it by default (privacy mode, covered below).
You don't strictly need a library: the Bot API is HTTP over JSON. A ready-made client does handle token masking, multipart uploads, error classification and retries, though. The examples below use the Telegram client from the async Socialift Python SDK (pip install socialift, Python 3.11+).
Create the bot in BotFather: 5 steps
Open @BotFather
Find the official @BotFather bot in Telegram and send /newbot.
Pick a name
The name shows in the contact card and can be changed later, e.g. “Store Assistant”.
Pick a username
5–32 characters, Latin letters, digits and underscores only, and it must end in “bot” (e.g. store_helper_bot). It can't be changed later, so choose carefully.
Store the token
BotFather returns a token like 110201543:AAHdqTc... Whoever holds it controls the bot. Keep it out of your code — use a TELEGRAM_BOT_TOKEN environment variable or a secrets manager.
Verify the token
If getMe (
bot.get_me()in the SDK) returns the bot profile, the token works. If it leaks, issue a new one with /token in BotFather; the old token stops working.
First request: verify the token and send a message
No token in code — Telegram.from_env() reads TELEGRAM_BOT_TOKEN.
import asyncio
from socialift import Telegram
async def main():
async with Telegram.from_env() as bot:
me = await bot.get_me()
print("Bot:", me["username"])
# chat_id is a numeric ID, or @username for a channel
await bot.send_message("@my_channel", "<b>Hello!</b> The bot is up.",
parse_mode="HTML")
asyncio.run(main())To post in a channel, the bot must be an admin there. In private chats you can only message users who already pressed /start — their numeric ID arrives in the update.
Message text is 1–4096 characters; a media caption is 0–1024. Split long text into several messages.
getUpdates or webhook: which one?
The two are mutually exclusive: while a webhook is set, getUpdates doesn't work. Undelivered updates are kept on Telegram's servers for no longer than 24 hours.
| Criterion | getUpdates (long polling) | Webhook |
|---|---|---|
| How it works | Your code asks Telegram and waits up to timeout seconds for a reply | Telegram POSTs each update to your HTTPS endpoint |
| Server requirements | No public address — works on a laptop | Public HTTPS with a valid SSL certificate; redirects aren't supported |
| Ports | Irrelevant | 443, 80, 88 or 8443 only |
| Concurrency | One process drains the queue | max_connections 1–100, default 40 |
| Acknowledgement | offset = last update_id + 1 | A 2XX response; otherwise Telegram retries |
| Best for | Local development, small bots | Production, serverless, many bots |
Local development: a getUpdates loop
from socialift import Telegram
async def run_polling():
async with Telegram.from_env() as bot:
await bot.delete_webhook() # getUpdates fails while a webhook is set
offset = None
while True:
updates = await bot.get_updates(offset=offset, timeout=25)
for update in updates:
offset = update["update_id"] + 1
msg = update.get("message") or {}
if msg.get("text") == "/start":
await bot.send_message(msg["chat"]["id"], "Welcome!")Recalculate offset after every response, or the same updates come back. Telegram's default timeout is 0 (short polling); use a positive value outside of testing.
Securing the webhook with secret_token
Telegram doesn't sign webhook bodies. Instead, the secret_token you pass to set_webhook is echoed in the X-Telegram-Bot-Api-Secret-Token header of every request, which is how you reject forged calls.
import os
from aiohttp import web
from socialift import Telegram
from socialift.webhooks import verify_telegram_request
SECRET = os.environ["TG_WEBHOOK_SECRET"] # 1–256 chars: A-Z, a-z, 0-9, _ and -
async def setup(bot: Telegram) -> None:
await bot.set_webhook("https://example.com/tg/webhook", secret_token=SECRET,
allowed_updates=["message", "callback_query"])
info = await bot.get_webhook_info()
print(info["pending_update_count"], info.get("last_error_message"))
async def handle(request: web.Request) -> web.Response:
if not verify_telegram_request(request.headers, SECRET):
return web.Response(status=403)
update = await request.json()
await queue.put(update) # heavy work goes to a queue; answer fast
return web.Response(text="ok")verify_telegram_request compares the header in constant time. Generate the secret with secrets.token_urlsafe(32) — it only uses allowed characters.
If nothing arrives, check last_error_message and pending_update_count from get_webhook_info() first. Telegram retries failed deliveries, so drop duplicates by update_id.
Photos, inline buttons and a reply keyboard
async def send_catalog(bot, chat_id: int) -> None:
inline = {"inline_keyboard": [[
{"text": "Catalog", "url": "https://example.com/catalog"},
{"text": "Place order", "callback_data": "order:42"},
]]}
await bot.send_photo(chat_id, "https://example.com/banner.jpg",
caption="New collection", reply_markup=inline)
menu = {"keyboard": [[{"text": "Contact"}, {"text": "Cart"}]],
"resize_keyboard": True}
await bot.send_message(chat_id, "Pick a section:", reply_markup=menu)
async def on_callback(bot, query: dict) -> None:
await bot.answer_callback_query(query["id"], text="Got it")callback_data is 1–64 bytes, so store a short key rather than a whole object. Always call answer_callback_query when a button is pressed; otherwise the user keeps seeing a progress indicator.
Reply keyboards (ReplyKeyboardMarkup) aren't supported in channels. A photo can be a file_id, an HTTPS URL or raw bytes.
File and message size limits
Values for the official Bot API server (api.telegram.org).
| Case | Limit |
|---|---|
| Message text | 1–4096 characters |
| Media caption | 0–1024 characters |
| Photo, multipart upload | 10 MB; width + height at most 10000, aspect ratio at most 20 |
| Other files, multipart upload | 50 MB |
| Sending by HTTPS URL | 5 MB for photos, 20 MB for other files |
Re-sending by file_id | no limit (file_id is unique per bot) |
| Downloading (getFile) | 20 MB; the link is valid for at least 1 hour |
| Your own local Bot API server | uploads up to 2000 MB, downloads unlimited |
Why can't my bot see group messages? (privacy mode)
Every bot added to a group runs in privacy mode by default. It then receives only commands addressed to it (/command@bot_name), messages sent via the bot, and replies to its own messages. Service messages, private chat messages and posts in channels the bot belongs to arrive regardless of the mode.
If the bot must see every group message, either make it an admin (admin bots receive everything) or disable the mode with /setprivacy in BotFather. After switching, remove the bot from the group and add it back, or the change won't take effect.
Telegram recommends disabling privacy mode only when truly necessary; forcing a reply to the bot's message is often enough. The bot's current mode is visible to everyone in the group member list.
Broadcast limits
Free limits from the Bots FAQ. Exceed them and Telegram returns HTTP 429 with parameters.retry_after.
| Target | Limit |
|---|---|
| A single chat | avoid more than 1 message per second; short bursts are tolerated |
| A group | no more than 20 messages per minute |
| Bulk notifications to different users | about 30 messages per second |
| With paid broadcasts | up to 1000 messages per second; each message above 30/s costs 0.1 Stars |
Notes and limits
Enabling paid broadcasts in BotFather requires at least 100,000 Stars on the bot's balance and at least 100,000 monthly active users. Otherwise Telegram suggests spreading broadcasts over a longer window, such as 8–12 hours.
Handling 429 correctly
import asyncio
import socialift
async def broadcast(bot, chat_ids: list[int], text: str) -> None:
for chat_id in chat_ids:
try:
await bot.send_message(chat_id, text)
except socialift.RateLimitError as exc:
await asyncio.sleep(exc.retry_after or 5)
await bot.send_message(chat_id, text)
except socialift.PermissionError:
mark_inactive(chat_id) # the user blocked the bot (403)
await asyncio.sleep(0.05) # ~20 msg/s, under the 30/s ceilingThe SDK turns a 429 into RateLimitError and retries on its own under the default RetryPolicy (3 attempts); once those are used up, the error reaches you. If retry_after is missing, fall back to a fixed pause.
A 403 (for example, the user blocked the bot) won't fix itself on retry — remove that chat from the list.
Before going to production
- The token lives in an environment variable or secrets manager and never appears in logs.
- The webhook is HTTPS with
secret_token, and the header is checked on every request. allowed_updateslists only the update types you handle (e.g. message, callback_query).- Duplicate updates are dropped by
update_id. - The webhook responds quickly; heavy processing runs on a queue.
- 429 and 403 are handled separately, and broadcasts stay below 30 messages per second.
- Privacy mode or admin rights match your group scenario.
Frequently asked questions
Does it cost money to create a Telegram bot?
No. Creating a bot in BotFather and using the Bot API are free. Costs only appear with optional features, such as paid broadcasts above 30 messages per second, which are paid in Telegram Stars.
Can a bot message a user first?
No. Telegram requires the user to message the bot first or add it to a group. Share your t.me/bot_name link on your site, in ads or as a QR code, and store the chat ID after /start.
What should I do if my bot token leaks?
Issue a new token with /token in BotFather; the old one stops working. Then update the environment variable on your server and set the webhook again. Never commit the token to git or ship it in frontend code.
My webhook is set but no updates arrive. What should I check?
Read last_error_message from getWebhookInfo first. Then confirm the port is 443, 80, 88 or 8443, the SSL certificate is valid and the URL doesn't redirect. Remember that getUpdates is disabled while a webhook is active.
How can a bot read every message in a group?
Make the bot a group admin, or disable privacy mode with /setprivacy in BotFather and re-add the bot to the group. Even then it won't receive other bots' messages, and the Bot API has no method for reading chat history — the bot only sees new updates.
Which Python library do I need for a Telegram bot?
Any HTTP client works, since the Bot API is HTTPS and JSON. A ready-made client takes care of uploads, error handling and retries: in the Socialift SDK that's the async Telegram client, with bot.call("methodName") for methods that have no dedicated wrapper.