Telegram Bot
Build a Telegram bot that sends messages, photos, documents, voice notes, and videos to a CloudStation project's AI agent.
A Telegram bot can use the Agents API as its AI backend. Users talk to your Telegram bot, your bot forwards messages to CloudStation, and CloudStation calls your bot's webhook when the assistant reply is ready.
This pattern is based on the cloudstation-telegram-bot boilerplate. The boilerplate supports text, photos, documents, voice notes, videos, stickers, and locations without requiring any AI provider SDKs.
Architecture
Telegram user
-> Telegram Bot API
-> your Node/Express bot using grammY polling
-> CloudStation Agents API
-> selected CloudStation agent
-> your /cloudstation-webhook callback
-> Telegram Bot API sendMessage
-> Telegram userThe bot has two jobs:
- Receive Telegram updates and convert them into Agents API messages.
- Receive CloudStation callbacks and send the assistant response back to Telegram.
The Telegram boilerplate uses grammY polling for Telegram messages, so you do not need to configure a Telegram webhook. It still exposes an HTTP endpoint for CloudStation response callbacks.
How It Works
Telegram receives a user message
The user sends text, a photo, a document, a voice note, a video, a sticker, or a location to your Telegram bot.
The bot builds an Agents API request
The bot creates:
conversation_idfrom the Telegram chat IDplatform: "telegram"senderfrom the Telegram userexternal_message_idfrom the Telegram chat/message ID (text messages)content_blocksfor media when neededresponse_webhookpointing to your bot's/cloudstation-webhook- an
Idempotency-Keyderived from the Telegram chat + message ID (telegram:<chat_id>:<message_id>, text messages)
CloudStation accepts the message
The Agents API returns status: "accepted", an adw_id, and a task_id.
CloudStation calls the bot back
When the assistant reply is ready, CloudStation POSTs to /cloudstation-webhook.
The bot sends the reply to Telegram
The bot extracts message.content from the callback and sends it to the original Telegram chat.
Environment Variables
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
WEBHOOK_PORT=3000
WEBHOOK_BASE_URL=https://your-bot.example.com
CLOUDSTATION_API_URL=https://charlie-back.cloud-station.io
CLOUDSTATION_PROJECT_ID=your_project_id
CLOUDSTATION_API_KEY=your_project_api_key
CLOUDSTATION_CLIENT_ID=your_project_client_id
CLOUDSTATION_AGENT_TEMPLATE_ID=optional_agent_template_id_or_name
CLOUDSTATION_WEBHOOK_SECRET=optional_project_webhook_secret
ESCALATION_CHAT_ID=optional_telegram_chat_id_for_handoff| Variable | Required | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN | Yes | Token from Telegram BotFather. |
WEBHOOK_PORT | No | HTTP port for /health and /cloudstation-webhook. Defaults to 3000. |
WEBHOOK_BASE_URL | Yes for hosted callbacks | Public base URL where CloudStation can reach this bot. |
CLOUDSTATION_API_URL | Yes | Agents API base URL, usually https://charlie-back.cloud-station.io. |
CLOUDSTATION_PROJECT_ID | Yes | Target project ID. |
CLOUDSTATION_API_KEY | Yes in production | API key for the project. |
CLOUDSTATION_CLIENT_ID | Yes in production | Client ID paired with the API key. |
CLOUDSTATION_AGENT_TEMPLATE_ID | No | Specific agent template UUID, name, or display name. |
CLOUDSTATION_WEBHOOK_SECRET | Recommended | Secret used to verify CloudStation callback signatures. |
ESCALATION_CHAT_ID | No | Telegram chat ID to notify when the agent emits an escalation marker. |
Legacy CHARLIE_* variable names (CHARLIE_API_URL, CHARLIE_API_KEY, CHARLIE_CLIENT_ID, CHARLIE_PROJECT_ID, CHARLIE_WEBHOOK_SECRET) are still read for backward compatibility.
CLOUDSTATION_API_KEY and CLOUDSTATION_CLIENT_ID must stay server-side. Do not put them in browser code or frontend environment variables.
Quick Start
Create a Telegram bot
Open Telegram and message @BotFather. Send /newbot, follow the prompts, and copy the bot token.
Configure environment variables
Create .env from the boilerplate example and fill in Telegram plus CloudStation credentials.
cp .env.example .envInstall dependencies
npm installRun locally
npm run devTest Telegram messages
Send your bot a text message. Then test a photo, document, or voice note.
Local vs Hosted Callback URLs
WEBHOOK_BASE_URL must be reachable by the CloudStation backend that sends the callback.
If CloudStation is running locally on the same machine as the bot:
WEBHOOK_BASE_URL=http://localhost:3000If CloudStation is hosted remotely, use a public URL:
WEBHOOK_BASE_URL=https://your-bot.example.comFor remote CloudStation calling a local laptop, use a tunnel such as Cloudflare Tunnel, ngrok, or deploy the bot as a CloudStation service.
Agents API Request Built by the Bot
The core client sends requests like this (the boilerplate posts the same body, plus conversation_id, to the legacy /v1/projects/{project_id}/api/message route):
{
"message": "Hello from Telegram",
"platform": "telegram",
"response_webhook": "https://your-bot.example.com/cloudstation-webhook",
"agent_template_id": "optional-agent-template",
"sender": {
"id": "123456789",
"name": "Jane Customer"
},
"external_message_id": "telegram:123456789:42"
}sent with headers including an idempotency key derived from the Telegram chat + message ID:
Idempotency-Key: telegram:123456789:42Telegram's own retry behavior on webhook/polling updates means the same message can be delivered more than once. Sending telegram:<chat_id>:<message_id> as Idempotency-Key guarantees the bot never triggers a duplicate agent run for the same message.
The TypeScript pattern:
const body: Record<string, unknown> = {
message,
platform: 'telegram',
response_webhook: `${webhookBaseUrl.replace(/\/+$/, '')}/cloudstation-webhook`,
};
if (agentTemplateId) body.agent_template_id = agentTemplateId;
if (sender) body.sender = sender;
if (contentBlocks.length > 0) body.content_blocks = contentBlocks;
if (externalMessageId) body.external_message_id = externalMessageId;
await fetch(`${apiUrl}/v1/projects/${projectId}/conversations/${conversationId}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'x-client-id': clientId,
...(externalMessageId ? { 'Idempotency-Key': externalMessageId } : {}),
},
body: JSON.stringify(body),
});Conversation IDs for Telegram
Use a stable conversation ID per Telegram chat.
Recommended format:
telegram-{chat_id}Examples:
telegram-123456789
telegram--1001234567890Telegram group/supergroup IDs can be negative. Keep the value stable so each chat continues the same CloudStation conversation.
External Message IDs for Telegram
Use external message IDs to trace and dedupe events.
Recommended format:
telegram:{chat_id}:{message_id}Example:
telegram:123456789:42Supported Message Types
| Telegram type | Agents API behavior |
|---|---|
| Text | Forwarded as message. |
| Photos | Downloaded, base64 encoded, sent as an image content block. |
| Documents | Downloaded, base64 encoded, sent as a document content block. |
| Voice notes | Downloaded, base64 encoded, sent as a document content block (audio/ogg). |
| Videos | Downloaded, base64 encoded, sent as a document content block (video/mp4) when 20 MB or smaller. |
| Stickers | Sent as text such as "User sent a sticker". |
| Location | Sent as text with latitude and longitude. |
Files larger than 20 MB should be described in text only instead of embedded as base64. This keeps Agents API requests reliable and avoids huge payloads.
Media Example
For a photo, send a text message plus an image block:
{
"message": "User sent a photo with caption: Is this invoice readable?",
"platform": "telegram",
"response_webhook": "https://your-bot.example.com/cloudstation-webhook",
"content_blocks": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "BASE64_IMAGE_DATA"
}
}
]
}Long-Running Replies
Some agent runs take longer than a user wants to wait on a webhook alone. Use the task_id from the 202 response to poll status directly:
GET /v1/projects/{project_id}/tasks/{task_id}Poll while the task is queued or running, and stop once it reaches completed, failed, canceled, or timed_out. waiting_input means the agent asked the user a question and waits for the next message. See Tasks & Events for the full status table and polling pattern.
Receiving Replies from CloudStation
Your bot exposes:
POST /cloudstation-webhookCloudStation sends the assistant response to that endpoint when ready.
Minimal callback handler:
app.post('/cloudstation-webhook', async (req, res) => {
res.sendStatus(200);
const payload = req.body;
const conversationId = payload?.conversation_id;
const content = payload?.message?.content;
if (!conversationId || !content) return;
if (payload.type === 'run.completed') return; // repeats the last reply
const chatId = conversationId.replace(/^telegram-/, '');
await bot.api.sendMessage(chatId, content);
});Production handlers should also verify callback signatures and dedupe delivery_id. See Agents API Webhooks.
Callback Signature Verification
If CLOUDSTATION_WEBHOOK_SECRET is set, verify:
X-CloudStation-Timestamp
X-CloudStation-Signature
X-CloudStation-Delivery-IDUse this signature formula:
v1=hex(HMAC_SHA256(secret, timestamp + "." + raw_request_body))The boilerplate verifies:
- timestamp exists
- signature exists and starts with
v1= - timestamp is within a 5 minute tolerance
- raw body exists
- signature is a valid 64-character hex HMAC
- comparison uses
timingSafeEqual
Escalation Pattern
The boilerplate supports a simple human handoff marker:
[ESCALATE: short summary of why human help is needed]If the assistant includes that marker at the end of its response, the bot can strip the marker from the user-facing reply and notify ESCALATION_CHAT_ID.
Use this only if your agent instructions tell the agent when and how to escalate.
Deploy to CloudStation
Deploy the bot service
Deploy the Telegram bot as a CloudStation service with a public URL.
Set production environment variables
Set WEBHOOK_BASE_URL to the public service URL.
WEBHOOK_BASE_URL=https://your-bot.example.comStart the bot
npm startTest from Telegram
Send a message to the bot and confirm the response arrives back in Telegram.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Bot does not start | Missing TELEGRAM_BOT_TOKEN or CloudStation credentials | Check .env. |
Agents API returns 401 | Bad CLOUDSTATION_API_KEY or CLOUDSTATION_CLIENT_ID | Regenerate or copy credentials from Settings. |
| Agents API accepts but no Telegram reply | WEBHOOK_BASE_URL is not reachable | Use public HTTPS URL or local tunnel. |
| Callback signature fails | Raw body not preserved | Configure Express JSON parser with verify callback. |
| Duplicate Telegram replies | Callback retried, or run.completed sent as a reply | Dedupe by delivery_id and skip run.completed. |
| Media fails | File too large or base64 includes data URL prefix | Limit size and send raw base64 only. |
| Wrong chat gets reply | Bad conversation_id mapping | Use telegram-{chat_id} consistently. |