Claude API Telegram Bot Integration Tutorial
Building a Telegram bot powered by the Claude API
If you want a Telegram bot that answers messages with Claude instead of canned responses, you need three pieces working together: a Telegram bot token from BotFather, a webhook (or long-polling loop) that receives chat updates, and an HTTP call to an API that talks to Claude. This tutorial walks through all three, from zero to a working bot that replies with streamed Claude output inside a Telegram chat.
The fastest path uses Telegram's Bot API for message handling and a standard HTTPS endpoint for the Claude call — no special SDK is required on either side. Below is a complete Node.js example you can deploy to any server or serverless function.
What you need before starting
- A Telegram bot token, created by messaging
@BotFatherand running/newbot. - An API key that can call Claude. You can use Anthropic's API directly, or route through a wrapper like SubToAPI if you want application-level keys (
sub_live_...), per-key usage tracking, and team seats without managing billing and infra yourself. - A public HTTPS URL for your bot's webhook (ngrok works fine for local testing).
- Node.js 18+ (for native
fetch).
Step 1: register a webhook with Telegram
Telegram pushes new messages to a URL you register. Set it once:
curl -X POST "https://api.telegram.org/bot<TELEGRAM_TOKEN>/setWebhook" \
-d "url=https://yourdomain.com/telegram/webhook"
Telegram will POST a JSON payload to that URL every time a user sends your bot a message.
Step 2: handle incoming messages
Set up a minimal Express endpoint that reads the user's text and the chat ID:
import express from "express";
const app = express();
app.use(express.json());
app.post("/telegram/webhook", async (req, res) => {
const message = req.body.message;
if (!message || !message.text) {
return res.sendStatus(200);
}
const chatId = message.chat.id;
const userText = message.text;
const reply = await askClaude(userText);
await sendTelegramMessage(chatId, reply);
res.sendStatus(200);
});
app.listen(3000);
Always respond 200 to Telegram quickly — it retries webhooks that time out, which can duplicate replies if your Claude call is slow.
Step 3: call Claude for a reply
Here's the core function, written against SubToAPI's endpoint (sub_live_... keys work exactly like a standard bearer token):
async function askClaude(userText) {
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 800,
messages: [{ role: "user", content: userText }],
}),
});
const data = await response.json();
return data.content[0].text;
}
The request shape matches the Messages API, so if you've used Claude before this will look familiar — see /docs/messages for the full parameter reference.
Step 4: send the reply back to Telegram
async function sendTelegramMessage(chatId, text) {
await fetch(
`https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: chatId, text }),
}
);
}
At this point you have a working loop: Telegram message in, Claude reply out. Deploy this behind any public URL and your bot responds to every chat message.
Keeping conversation context
Telegram doesn't track conversation history for you — each webhook call is a fresh message with no memory of what came before. For a bot that holds a real conversation, you need to store prior turns (in Redis, a database, or even an in-memory map keyed by chatId for a quick prototype) and replay them as the messages array on every request:
const history = conversations.get(chatId) || [];
history.push({ role: "user", content: userText });
const data = await askClaudeWithHistory(history);
history.push({ role: "assistant", content: data.content[0].text });
conversations.set(chatId, history.slice(-20)); // cap history length
Trim history regularly — Claude's context window is large but not unlimited, and every stored turn adds to the tokens billed on each request.
Streaming replies for long answers
Telegram doesn't support token-by-token streaming the way a chat UI does, but you can still stream from Claude server-side and edit the Telegram message as chunks arrive, which feels much more responsive than waiting for a full response:
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 800,
stream: true,
messages: [{ role: "user", content: userText }],
}),
});
Buffer the stream, update a placeholder Telegram message every ~500ms with editMessageText, and finalize it when the stream closes. Details on event types and parsing are in /docs/streaming.
Adding tool use for real actions
If your bot needs to do more than chat — look up order status, hit an internal API, query a database — define tools in the request and handle the tool_use blocks Claude returns before sending a final reply to the user. This is standard function-calling behavior; see /docs/tools for the request/response shapes and multi-turn tool loops.
Production considerations
A few things matter once real users are hitting the bot:
- Rate limiting per chat. Users can spam messages faster than Claude can reply. Queue or debounce per
chatId. - Key management. If multiple bots or team members share Claude access, issue separate application keys per bot rather than one shared secret — SubToAPI's dashboard handles this with per-key usage metadata so you can see which bot or environment is driving cost.
- Error handling. Catch timeouts and rate-limit responses from the Claude call and send the user a friendly fallback message instead of letting the webhook hang.
- Logging. Keep a record of prompts and responses for debugging, especially while tuning system prompts for bot tone and behavior.
If you want the key management and billing side handled without building your own proxy, sign up for a trial at /signup and check /pricing for plan details — the /docs/quickstart page walks through generating your first sub_live_... key.
questions
Do I need Anthropic's SDK to build a Telegram bot with Claude? No. The Messages API is a plain HTTPS JSON endpoint, so fetch or curl from any language works fine — the examples above use no SDK at all.
Can the bot remember previous messages in a Telegram chat? Not automatically. Telegram sends each message independently, so you must store conversation history yourself (per chatId) and resend it as the messages array on each Claude request.
What's the simplest way to test the webhook before deploying? Use ngrok http 3000 to expose your local server, register that URL with setWebhook, and message your bot directly — no deployment needed for initial testing.