Short path: Chatbot API · Multi-model API · OpenAI-compatible API · Python API guide
Last verified: August 3, 2026
A Telegram AI bot needs two server-side connections: Telegram's Bot API receives and sends messages, while an OpenAI-compatible API generates the answer. The user's phone never receives either secret. This tutorial uses long polling for a simple local deployment and LumeAPI for the model call.
The first version intentionally handles text only. Files, voice, memory and tools should be added after authentication, duplicate handling and cost limits work.
Create the bot and keep both tokens private
Message @BotFather in Telegram, run /newbot and follow the prompts. Store the returned bot token like a password; anyone who has it controls the bot.
Create environment variables on the machine that will run the code:
TELEGRAM_BOT_TOKEN=replace_me
LUMEAPI_API_KEY=replace_meThe LumeAPI key must be an inference key. Do not use the Research article-ingest key.
Install the Python dependencies
python -m venv .venv
# Windows: .venv\Scripts\activate
# macOS/Linux: source .venv/bin/activate
pip install openai requestsRun a minimal polling bot
Save this as bot.py:
import os
import time
from typing import Any
import requests
from openai import OpenAI
TELEGRAM_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
LUMEAPI_KEY = os.environ["LUMEAPI_API_KEY"]
TELEGRAM_API = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}"
ai = OpenAI(
api_key=LUMEAPI_KEY,
base_url="https://api.lumeapi.site/v1",
timeout=45.0,
max_retries=2,
)
def telegram(method: str, payload: dict[str, Any]) -> dict[str, Any]:
response = requests.post(
f"{TELEGRAM_API}/{method}",
json=payload,
timeout=40,
)
response.raise_for_status()
data = response.json()
if not data.get("ok"):
raise RuntimeError(f"Telegram rejected {method}: {data}")
return data
def answer_question(question: str) -> str:
result = ai.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{
"role": "system",
"content": "Answer helpfully and concisely. Do not invent private context.",
},
{"role": "user", "content": question[:4000]},
],
temperature=0.2,
)
answer = result.choices[0].message.content
if not answer:
raise RuntimeError("The model returned no text.")
return answer.strip()
def run() -> None:
offset: int | None = None
while True:
try:
payload: dict[str, Any] = {
"timeout": 30,
"allowed_updates": ["message"],
}
if offset is not None:
payload["offset"] = offset
response = requests.post(
f"{TELEGRAM_API}/getUpdates",
json=payload,
timeout=40,
)
response.raise_for_status()
updates = response.json().get("result", [])
for update in updates:
offset = update["update_id"] + 1
message = update.get("message", {})
text = message.get("text")
chat_id = message.get("chat", {}).get("id")
if not text or chat_id is None:
continue
if text == "/start":
telegram("sendMessage", {
"chat_id": chat_id,
"text": "Send a question and I will answer it.",
})
continue
try:
reply = answer_question(text)
telegram("sendMessage", {
"chat_id": chat_id,
"text": reply[:4000],
})
except Exception as exc:
print("message failed", type(exc).__name__)
telegram("sendMessage", {
"chat_id": chat_id,
"text": "The AI request failed. Please try again later.",
})
except requests.RequestException as exc:
print("poll failed", type(exc).__name__)
time.sleep(3)
if __name__ == "__main__":
run()Start the bot:
python bot.pyOpen the bot in Telegram, send /start, then ask a short question.
Why offset matters
Telegram assigns every update an update_id. Sending the next expected ID as offset confirms earlier updates and prevents the same message from being returned forever during one process run.
The example keeps offset in memory. For production, persist the last acknowledged update ID or store processed message IDs. Decide whether to acknowledge before or after the model call based on your failure policy:
- Acknowledge first: avoids duplicate model charges but may drop a reply after a crash.
- Acknowledge after: enables recovery but can repeat the model call and message.
A durable job table with a unique update ID gives the best result: receive once, process with bounded retries and send once.
Long polling versus webhooks
Long polling is simplest for local development and one worker. A webhook is better when you already operate a public HTTPS service or need horizontal scaling. Telegram documents both through the same Bot API.
Do not run polling and a webhook for the same bot simultaneously. Also avoid starting two polling processes with one token; they can compete for updates.
Add user and cost controls before sharing the bot
The public bot username is discoverable. Before inviting users:
- Allowlist user or chat IDs for a private tool.
- Add per-user and global rate limits.
- Limit prompt size and generated output.
- Keep a model allowlist.
- Do not log full messages by default.
- Define conversation retention and deletion behavior.
- Moderate input and output for the community.
- Require explicit approval before any external tool action.
Telegram bots cannot initiate a conversation with a user; the user must contact or add the bot first. In groups, Privacy Mode affects which messages a bot can see. Keep it enabled unless the use case genuinely requires broader access.
Handle Telegram and model rate limits separately
Telegram and the model gateway have independent limits. A 429 response can include retry guidance. Do not place an unlimited while True retry inside each message handler. Use a queue, a maximum attempt count and jitter.
If the bot becomes busy, bound concurrent model calls and tell the user the request is queued. One user should not be able to consume the entire API budget by sending many messages at once.
Useful next steps
- Add short, consented conversation memory keyed by chat ID.
- Add retrieval from approved support documents.
- Route simple and difficult questions to different models through the multi-model API.
- Split long replies at paragraph boundaries instead of silently truncating.
- Store request ID, latency, tokens and outcome for cost-per-resolved-question reporting.
- Move from polling to a verified HTTPS webhook when deployment requires it.
Sources and verification boundary
The Python code was syntax-checked on August 3, 2026. End-to-end execution requires a Telegram bot token and a LumeAPI inference key, so duplicate behavior, privacy settings and real billing must be tested in your own bot before public use.