Back to News
Tutorials

Step-by-Step: Telegram Bot API Webhook Integration

Telegram Technical TeamNovember 08, 2025417 views
APIwebhookbotintegrationmessagedeployment
Telegram Bot API webhook setup, configure Telegram webhook URL, handle Telegram updates, setWebhook example, Telegram bot SSL certificate, webhook vs getUpdates, Node.js Telegram bot webhook, Python telegram-bot webhook, troubleshoot Telegram webhook 404, secure Telegram bot endpoint

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 blocks api.telegram.org on 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

  1. Open Telegram, search @BotFather, type /newbot.
  2. Choose a display name and a username ending in bot.
  3. Copy the token (format 1234567890:AAHhi...). Treat it like a password—store it in env variable BOT_TOKEN.
  4. Run /setinline and /setinlinegeo if you plan inline queries; they do not affect webhooks.
  5. 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

AspectWebhookgetUpdates
Latency~50 ms~1–2 s (because of long polling interval)
FirewallPort 443 inbound must be openOnly outbound 443 needed
Max payloadSame (up to 2 GB files)Same
ScalingHorizontal (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

  1. Install ngrok; run ngrok http 8443.
  2. Copy the https://<uuid>.ngrok.io tunnel URL.
  3. Set webhook: curl -F "url=https://<uuid>.ngrok.io/bot<TOKEN>" ...
  4. Keep the terminal open; free tunnels expire in 2 h.
  5. Use -region eu flag 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/20 and 91.108.4.0/22. Add iptables or Cloudflare firewall rules.
  • TLS fingerprint: Force cipher suite TLS_AES_256_GCM_SHA384 to 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 /getProxyConfig from @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: -M parameter 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:1080 in 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_zone with rate=50r/s per IP (Telegram may batch updates from different users).
  • Add fail2ban regex for 403 entries triggered by forged headers.
  • Return 429 with Retry-After header; 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 {