How to Build a Telegram Bot with the Claude API in 2026 (Step-by-Step) ⏱️ 10 min read
A Telegram bot backed by Claude is one of the most useful personal tools you can ship in an afternoon. Unlike a web app, it requires no frontend, no auth system, and no hosting complexity — just a Python script, a webhook, and two API keys. I’ve been running a personal Claude bot for daily research tasks for six months, and the setup I’ll walk through here is the exact stack I use in production.
What You’ll Build
By the end of this guide, you’ll have a Telegram bot that:
- Responds to any message with a Claude-powered reply
- Maintains per-user conversation history (multi-turn memory)
- Handles photo uploads with Claude’s vision capabilities
- Runs on a free Railway or Fly.io instance (or your own VPS)
Stack: Python 3.11, python-telegram-bot 21.x, Anthropic Python SDK, and SQLite for conversation history.
Step 1: Create Your Telegram Bot
Open Telegram and search for @BotFather. Send /newbot, follow the prompts to name your bot, and copy the token it gives you. That token is your TELEGRAM_TOKEN — treat it like a password. Set a description with /setdescription and a profile photo with /setuserpic so the bot looks intentional.
Step 2: Set Up Your Project
mkdir claude-telegram-bot && cd claude-telegram-bot
python3 -m venv venv && source venv/bin/activate
pip install python-telegram-bot anthropic python-dotenv
Create a .env file:
TELEGRAM_TOKEN=your_botfather_token_here
ANTHROPIC_API_KEY=your_claude_api_key_here
ALLOWED_USER_IDS=123456789,987654321
Get your Telegram user ID by messaging @userinfobot. The ALLOWED_USER_IDS restriction is critical — without it, anyone who finds your bot can run up your Anthropic bill.
Step 3: The Core Bot Code
import os
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
import anthropic
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
ALLOWED_USERS = set(int(uid) for uid in os.environ.get("ALLOWED_USER_IDS", "").split(",") if uid)
conversation_history: dict[int, list] = {}
SYSTEM_PROMPT = "You are a concise personal assistant in Telegram. Use bullet points for lists."
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Claude is ready. Send me anything.")
async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE):
conversation_history[update.effective_user.id] = []
await update.message.reply_text("History cleared.")
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
if ALLOWED_USERS and user_id not in ALLOWED_USERS:
await update.message.reply_text("Access denied.")
return
history = conversation_history.setdefault(user_id, [])
history.append({"role": "user", "content": update.message.text})
if len(history) > 20:
history = history[-20:]
conversation_history[user_id] = history
await context.bot.send_chat_action(chat_id=update.effective_chat.id, action="typing")
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=history
)
reply = response.content[0].text
history.append({"role": "assistant", "content": reply})
await update.message.reply_text(reply)
def main():
app = Application.builder().token(os.environ["TELEGRAM_TOKEN"]).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("clear", clear))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
Run locally with python bot.py and send your bot a message. You’ll get a Claude response within 2-3 seconds.
Step 4: Add Vision Support for Photo Uploads
Add a photo handler alongside the text one:
async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
if ALLOWED_USERS and user_id not in ALLOWED_USERS:
return
import base64
photo = update.message.photo[-1] # highest resolution
photo_file = await context.bot.get_file(photo.file_id)
photo_bytes = await photo_file.download_as_bytearray()
image_b64 = base64.standard_b64encode(photo_bytes).decode("utf-8")
caption = update.message.caption or "What is in this image?"
history = conversation_history.setdefault(user_id, [])
history.append({
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}},
{"type": "text", "text": caption}
]
})
await context.bot.send_chat_action(chat_id=update.effective_chat.id, action="typing")
response = client.messages.create(model="claude-sonnet-4-5", max_tokens=1024,
system=SYSTEM_PROMPT, messages=history)
reply = response.content[0].text
history.append({"role": "assistant", "content": reply})
await update.message.reply_text(reply)
# In main(), add: app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
Step 5: Deploy to Production
For a personal bot, Railway is the simplest path. Create a Procfile:
worker: python bot.py
Push to GitHub, connect to Railway, add your environment variables in the dashboard, and deploy. The free tier gives $5/month in credits — enough for a personal bot at low volume. For a VPS, run the bot inside a systemd service. The polling architecture requires no inbound port or webhook URL — the bot long-polls Telegram’s servers directly.
Cost Reality Check
A personal Claude bot used 20-30 times per day with typical 500-token exchanges costs roughly $0.50-$1.50/month using Claude Haiku, or $3-8/month with Claude Sonnet. That’s less than a coffee for a personal AI assistant that knows your full conversation history.
The 20-message history cap is intentional — uncapped history pushes input tokens into expensive territory. For longer memory, implement summarization: when history exceeds 20 messages, call Claude once to produce a 200-token summary, replace the full history with the summary as a system note, and continue. Effective memory stays intact at a fraction of the cost.
What to Build Next
This foundation handles the hard parts. Natural extensions: a /remind command backed by APScheduler, tool use so Claude can search the web or query your Notion database, or a group bot that only responds when mentioned. The full working code is under 100 lines — fork it, customize the system prompt to your use case, and you’ll have a more useful assistant than most $20/month subscriptions. Because this one knows your context and remembers yesterday’s conversation.