Cloudstation

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 user

The bot has two jobs:

  1. Receive Telegram updates and convert them into Agents API messages.
  2. 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_id from the Telegram chat ID
  • platform: "telegram"
  • sender from the Telegram user
  • external_message_id from the Telegram chat/message ID (text messages)
  • content_blocks for media when needed
  • response_webhook pointing to your bot's /cloudstation-webhook
  • an Idempotency-Key derived 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
VariableRequiredDescription
TELEGRAM_BOT_TOKENYesToken from Telegram BotFather.
WEBHOOK_PORTNoHTTP port for /health and /cloudstation-webhook. Defaults to 3000.
WEBHOOK_BASE_URLYes for hosted callbacksPublic base URL where CloudStation can reach this bot.
CLOUDSTATION_API_URLYesAgents API base URL, usually https://charlie-back.cloud-station.io.
CLOUDSTATION_PROJECT_IDYesTarget project ID.
CLOUDSTATION_API_KEYYes in productionAPI key for the project.
CLOUDSTATION_CLIENT_IDYes in productionClient ID paired with the API key.
CLOUDSTATION_AGENT_TEMPLATE_IDNoSpecific agent template UUID, name, or display name.
CLOUDSTATION_WEBHOOK_SECRETRecommendedSecret used to verify CloudStation callback signatures.
ESCALATION_CHAT_IDNoTelegram 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 .env

Run locally

npm run dev

Test 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:3000

If CloudStation is hosted remotely, use a public URL:

WEBHOOK_BASE_URL=https://your-bot.example.com

For 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:42

Telegram'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--1001234567890

Telegram 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:42

Supported Message Types

Telegram typeAgents API behavior
TextForwarded as message.
PhotosDownloaded, base64 encoded, sent as an image content block.
DocumentsDownloaded, base64 encoded, sent as a document content block.
Voice notesDownloaded, base64 encoded, sent as a document content block (audio/ogg).
VideosDownloaded, base64 encoded, sent as a document content block (video/mp4) when 20 MB or smaller.
StickersSent as text such as "User sent a sticker".
LocationSent 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-webhook

CloudStation 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-ID

Use 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.com

Start the bot

npm start

Test from Telegram

Send a message to the bot and confirm the response arrives back in Telegram.

Troubleshooting

SymptomLikely causeFix
Bot does not startMissing TELEGRAM_BOT_TOKEN or CloudStation credentialsCheck .env.
Agents API returns 401Bad CLOUDSTATION_API_KEY or CLOUDSTATION_CLIENT_IDRegenerate or copy credentials from Settings.
Agents API accepts but no Telegram replyWEBHOOK_BASE_URL is not reachableUse public HTTPS URL or local tunnel.
Callback signature failsRaw body not preservedConfigure Express JSON parser with verify callback.
Duplicate Telegram repliesCallback retried, or run.completed sent as a replyDedupe by delivery_id and skip run.completed.
Media failsFile too large or base64 includes data URL prefixLimit size and send raw base64 only.
Wrong chat gets replyBad conversation_id mappingUse telegram-{chat_id} consistently.

Next Steps