Step-by-Step: Telegram Bot API Webhook Integration

1. Prerequisites and Environment Check
Before touching the Bot API, ensure you have:
- A Telegram account that can message
@BotFather. Disable any corporate firewall that blocksapi.telegram.orgon port 443. - A publicly reachable HTTPS endpoint (self-signed certificates are rejected). Use Let’s Encrypt or Cloudflare Origin CA.
- Python ≥ 3.9 or Node.js ≥ 18. Both are shown; pick one.
- OpenSSL ≥ 1.1.1 for TLS 1.3 and ALPN (HTTP/2) support.
Run openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -tls1_3; if you see “Verify return code: 0 (ok)”, your cert chain is clean.
2. Creating the Bot and Token
- Open Telegram, search
@BotFather, type/newbot. - Choose a display name and a username ending in bot.
- Copy the token (format
1234567890:AAHhi...). Treat it like a password—store it in env variableBOT_TOKEN. - Run
/setinlineand/setinlinegeoif you plan inline queries; they do not affect webhooks. - Run
/mybots→ Bot Settings → “Allow groups?” → Enable if your bot must read group messages.
Note: The token is valid for the lifetime of the bot unless revoked via /revoke.
3. Webhook vs. getUpdates Polling
| Aspect | Webhook | getUpdates |
|---|---|---|
| Latency | ~50 ms | ~1–2 s (because of long polling interval) |
| Firewall | Port 443 inbound must be open | Only outbound 443 needed |
| Max payload | Same (up to 2 GB files) | Same |
| Scaling | Horizontal (add nodes behind LB) | Single process unless you shard update_id |
Choose webhook for production, polling for quick prototypes or restricted NAT environments.
4. Building a Minimal Python Webhook Endpoint
4.1 Install dependencies
python -m venv venv source venv/bin/activate pip install flask gunicorn cryptography
4.2 app.py
import os, hmac, hashlib, json
from flask import Flask, request, abort
TOKEN = os.environ['BOT_TOKEN']
SECRET = os.environ.get('SECRET_TOKEN') # optional but recommended
app = Flask(__name__)
def verify(header_sign, body):
if not SECRET: return True
mac = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, header_sign.replace('sha256=', ''))
@app.post('/bot' + TOKEN)
def webhook():
if not verify(request.headers.get('X-Telegram-Bot-Api-Secret-Token', ''), request.get_data()):
abort(403)
update = request.json
if 'message' in update:
chat_id = update['message']['chat']['id']
send_text(chat_id, 'Echo: ' + update['message'].get('text', ''))
return '', 200
def send_text(chat_id, text):
import requests
url = f'https://api.telegram.org/bot{TOKEN}/sendMessage'
requests.post(url, json={'chat_id': chat_id, 'text': text}, timeout=10)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8443, ssl_context='adhoc') # dev only
The path contains the token so that random scanners hit 404 instead of your handler.
5. Building the Same in Node.js (TypeScript)
npm init -y npm install express crypto axios dotenv
// index.ts
import express from 'express';
import crypto from 'crypto';
import axios from 'axios';
const TOKEN = process.env.BOT_TOKEN!;
const SECRET = process.env.SECRET_TOKEN!;
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post(`/bot${TOKEN}`, (req, res) => {
const sig = req.header('X-Telegram-Bot-Api-Secret-Token') || '';
if (SECRET) {
const hmac = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig.replace('sha256=', '')), Buffer.from(hmac))) {
return res.status(403).send('HMAC invalid');
}
}
const upd = JSON.parse(req.body.toString());
if (upd.message) {
const chatId = upd.message.chat.id;
axios.post(`https://api.telegram.org/bot${TOKEN}/sendMessage`, {
chat_id: chatId,
text: `Echo: ${upd.message.text || ''}`
});
}
res.sendStatus(200);
});
app.listen(8443, () => console.log('Node webhook on 8443'));
Compile with tsc or run directly via ts-node.
6. Setting the Webhook URL
The API endpoint is https://api.telegram.org/bot<TOKEN>/setWebhook.
6.1 cURL example
curl -F "url=https://yourdomain.com/bot<TOKEN>" \
-F "secret_token=myRandom32bytes" \
-F "max_connections=40" \
-F "allowed_updates[]=[\"message\",\"callback_query\",\"edited_message\"]" \
https://api.telegram.org/bot<TOKEN>/setWebhook
6.2 Python helper
import requests, json
cfg = {
'url': 'https://yourdomain.com/bot'+TOKEN,
'secret_token': 'myRandom32bytes',
'max_connections': 40,
'allowed_updates': ['message','callback_query','inline_query']
}
print(requests.post(f'https://api.telegram.org/bot{TOKEN}/setWebhook', json=cfg).json())
Successful response: {"ok":true,"result":true,"description":"Webhook was set"}
7. Local Development with ngrok
- Install
ngrok; runngrok http 8443. - Copy the
https://<uuid>.ngrok.iotunnel URL. - Set webhook:
curl -F "url=https://<uuid>.ngrok.io/bot<TOKEN>" ... - Keep the terminal open; free tunnels expire in 2 h.
- Use
-region euflag for lower RTT if you are in Europe.
ngrok prints each request; use -inspect=false to hide sensitive payloads.
8. Validating Incoming Updates
- Secret token: Already shown; constant-time compare.
- IP allow-list: Telegram publishes CIDR blocks
149.154.160.0/20and91.108.4.0/22. Add iptables or Cloudflare firewall rules. - TLS fingerprint: Force cipher suite
TLS_AES_256_GCM_SHA384to block sloppy bots. - Reject old messages:
if (date < time() - 30) ignore;
Telegram does not sign the JSON body; instead rely on the secret_token header plus IP allow-list for defense in depth.
9. Handling Files Up to 2 GB (4 GB in beta)
When a user sends a file, the update contains file_id but not the blob. Retrieve the file path:
file_info = requests.get(f'https://api.telegram.org/bot{TOKEN}/getFile?file_id={file_id}').json()
file_path = file_info['result']['file_path']
download_url = f'https://api.telegram.org/file/bot{TOKEN}/{file_path}'
Stream the file to avoid high RAM usage:
with requests.get(download_url, stream=True) as r:
r.raise_for_status()
with open(local_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
Uploading: use multipart/form-data with sendDocument/sendVideo/sendAudio; max 50 MB via direct form, else use URL upload with attach:// or serve the file on your server and pass file_url=https://yourcdn/file.mp4.
10. Responding with Inline Keyboards
keyboard = {
'inline_keyboard': [[
{'text': 'Yes', 'callback_data': 'vote_yes'},
{'text': 'No', 'callback_data': 'vote_no'}
]]
}
requests.post(f'https://api.telegram.org/bot{TOKEN}/sendMessage',
json={'chat_id': chat_id, 'text': 'Do you agree?', 'reply_markup': keyboard})
Telegram displays the buttons below the message. When pressed, your webhook receives:
{"callback_query": {"id": "123","from": {...},"message": {...},"data": "vote_yes"}}
Answer within 30 s to avoid “typing” spinner:
requests.post(f'https://api.telegram.org/bot{TOKEN}/answerCallbackQuery',
json={'callback_query_id': query_id, 'text': 'Thanks!'})
11. Deploying with Docker and systemd
Dockerfile (Python)
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY app.py . CMD ["gunicorn", "-b", "0.0.0.0:8443", "--workers", "4", "--threads", "2", "--keyfile", "/run/secrets/key.pem", "--certfile", "/run/secrets/cert.pem", "app:app"]
docker-compose.yml
version: "3.9"
services:
bot:
build: .
secrets:
- cert
- key
- token
environment:
BOT_TOKEN_FILE: /run/secrets/token
SECRET_TOKEN: myRandom32bytes
ports:
- "8443:8443"
secrets:
cert:
file: ./fullchain.pem
key:
file: ./privkey.pem
token:
file: ./bot_token.txt
systemd service
[Unit] Description=Telegram Bot Webhook After=network.target [Service] Type=exec WorkingDirectory=/opt/tgbot ExecStart=/usr/bin/docker compose up --build ExecStop=/usr/bin/docker compose down Restart=always RestartSec=10 [Install] WantedBy=multi-user.target
12. Nginx Reverse-Proxy and HTTP/2
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
client_max_body_size 4G;
location /bot<TOKEN> {
proxy_pass https://localhost:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Telegram-Bot-Api-Secret-Token $http_x_telegram_bot_api_secret_token;
proxy_read_timeout 30s;
}
}
Reload: sudo nginx -t && sudo systemctl reload nginx
13. IPv6 and MTProto-over-TLS Anti-Censorship
If your audience is in regions with DPI blocks, add Telegram’s official proxy chain to your outbound:
- Request
/getProxyConfigfrom@MTProxybot; it returns a secret and a list of DC IPs. - Compile mtproxy (
https://github.com/TelegramMessenger/MTProxy) on your gateway. - Enable fake-TLS mode:
-Mparameter and fake domain like cloudfront.net. - Tunnel only the bot traffic (api.telegram.org) through the proxy by setting
HTTPS_PROXY=socks5://127.0.0.1:1080in your container.
Your webhook server itself is not proxied; only outbound calls to Telegram API go via MTProto + TLS 1.3, lowering detection rates below 1 %.
14. Rate Limiting and DDoS Protection
- Bot API limits: 30 msg/s per chat, 1 msg/s globally to different chats, but no hard cap on incoming updates. Protect yourself:
- Use
nginx limit_req_zonewithrate=50r/sper IP (Telegram may batch updates from different users). - Add
fail2banregex for 403 entries triggered by forged headers. - Return
429withRetry-Afterheader; Telegram retries three times with exponential back-off.
15. Monitoring and Logging
import logging, json
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logger = logging.getLogger(__name__)
def log_update(update):
logger.info(json.dumps(update, ensure_ascii=False))
Ship logs to Loki or CloudWatch; redact file_id if you log media.
Add health check endpoint /ping returning {


