---
title: Telegram
description: Telegram adapter for Chat SDK with webhook and polling modes.
tagline: Connect to Telegram with support for groups, channels, inline keyboards, and a polling fallback for local development.
package: @chat-adapter/telegram
---

# Telegram



## Install

<PackageInstall package="@chat-adapter/telegram" />

## Quick start

<Callout type="info">
  The adapter auto-detects `TELEGRAM_ALLOWED_USER_IDS`, `TELEGRAM_BOT_TOKEN`, `TELEGRAM_WEBHOOK_SECRET_TOKEN`, `TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS`, `TELEGRAM_BOT_USERNAME`, and `TELEGRAM_MENTION_ON_REPLY` from the environment.
</Callout>

```typescript title="lib/bot.ts" lineNumbers
import { Chat } from "chat";
import { createTelegramAdapter } from "@chat-adapter/telegram";

const bot = new Chat({
  userName: "mybot",
  adapters: {
    telegram: createTelegramAdapter(),
  },
});

bot.onNewMention(async (thread, message) => {
  await thread.post(`You said: ${message.text}`);
});
```

```typescript title="app/api/webhooks/telegram/route.ts" lineNumbers
import { bot } from "@/lib/bot";

export async function POST(request: Request): Promise<Response> {
  return bot.webhooks.telegram(request);
}
```

Configure your bot webhook in BotFather / via the Telegram API:

```bash
curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-domain.com/api/webhooks/telegram",
    "secret_token": "your-secret-token"
  }'
```

## Vercel Connect

Use Vercel Connect for the outbound Telegram bot token:

```typescript title="lib/bot.ts" lineNumbers
import { createTelegramAdapter } from "@chat-adapter/telegram";
import { connectTelegramAdapter } from "@vercel/connect/chat";

const telegram = createTelegramAdapter({
  ...connectTelegramAdapter("telegram/acme-telegram"),
  secretToken: process.env.TELEGRAM_WEBHOOK_SECRET_TOKEN,
});
```

<Callout type="info">
  Connect does not forward Telegram webhooks. Keep
  `TELEGRAM_WEBHOOK_SECRET_TOKEN` for native webhook verification, or use
  polling mode without an inbound webhook. `TELEGRAM_BOT_TOKEN` is not needed
  when using `connectTelegramAdapter`.
</Callout>

## Configuration

<TypeTable
  type={{
  allowUnverifiedWebhooks: {
    type: "boolean",
    default: "false",
    description:
      "Accept webhook requests without secret-token verification. Auto-detected from `TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS=true`. Use only for local development or behind a trusted verifying proxy.",
  },
  allowedUserIds: {
    type: "Array<number | string>",
    description:
      "Telegram user IDs allowed to trigger the adapter. Auto-detected from `TELEGRAM_ALLOWED_USER_IDS` (comma-separated). All users are allowed when omitted or empty.",
  },
  botToken: {
    type: "string | (() => string | Promise<string>)",
    description:
      "Telegram bot token or resolver invoked for each Bot API request. Auto-detected from `TELEGRAM_BOT_TOKEN`.",
  },
  secretToken: {
    type: "string",
    description:
      "Webhook secret required in webhook mode unless unverified webhooks are explicitly allowed. Auto-detected from `TELEGRAM_WEBHOOK_SECRET_TOKEN`.",
  },
  mode: {
    type: '"auto" | "webhook" | "polling"',
    default: '"auto"',
    description:
      "Adapter mode. `auto` uses webhooks on serverless and polling everywhere else.",
  },
  longPolling: {
    type: "LongPollingOptions",
    description:
      "Long polling tuning. Fields: `timeout`, `limit`, `allowedUpdates`, `deleteWebhook`, `dropPendingUpdates`, `retryDelayMs`.",
  },
  businessMode: {
    type: "boolean",
    default: "false",
    description:
      "Enable Telegram Business mode for Connected Business Bots. Handles `business_connection`, `business_message`, and `edited_business_message` updates and passes `business_connection_id` on outbound API calls.",
  },
  mentionOnReply: {
    type: "boolean",
    default: "false",
    description:
      "Treat a reply to one of the bot's own messages as a mention, so it routes to `onNewMention`. Telegram users usually continue a conversation by replying instead of repeating `@name`. Auto-detected from `TELEGRAM_MENTION_ON_REPLY=true`. Implicit forum-topic replies and the bot's own messages never count.",
  },
  userName: {
    type: "string",
    description:
      "Bot username for mention detection. Auto-detected from `TELEGRAM_BOT_USERNAME` or `getMe`.",
  },
  nativeStreaming: {
    type: "boolean",
    default: "false",
    description:
      "Use Telegram's native draft previews for streamed posts in private chats. Defaults to post-and-edit in every chat type. Config only, no env var.",
  },
  streamingEditIntervalMs: {
    type: "number",
    default: "1100 private, 3100 other",
    description:
      "Minimum interval between edits on the post-and-edit streaming path. Acts as a floor, so a lower Chat-level `streamingUpdateIntervalMs` is raised to this value. Config only, no env var.",
  },
  apiUrl: {
    type: "string",
    description:
      "Override the Telegram API base URL. Auto-detected from `TELEGRAM_API_BASE_URL`.",
  },
}}
/>

`botToken` is always required. Webhook mode also requires `secretToken` unless `allowUnverifiedWebhooks` is explicitly enabled. Polling mode does not require webhook verification.

## Business mode

Telegram [Connected Business Bots](https://core.telegram.org/api/bots/connected-business-bots) let a bot reply to customer messages on behalf of a business account. Enable it with `businessMode: true`:

```typescript title="lib/bot.ts" lineNumbers
const telegram = createTelegramAdapter({
  businessMode: true,
});
```

Business threads use the ID format `telegram:biz:{connectionId}:{chatId}`. Each business conversation is its own channel, separate from any direct chat the same customer has with the bot. Outbound sends, edits, typing, file uploads, and threads created from inline-keyboard callbacks include `business_connection_id`. Slash commands from business chats reach `onSlashCommand` like any other chat.

The adapter ignores messages typed by the business owner and messages the bot sent on the account's behalf, and it stops replying when the connection is disabled or loses `can_reply`. Connection state lives in your state adapter, so a change reaches every instance.

A few Bot API limits apply to business threads:

* `delete()` uses `deleteBusinessMessages`, which needs the `can_delete_sent_messages` right.
* Reactions are not supported. `addReaction` and `removeReaction` throw a `NotImplementedError`.
* `fetchThread()` falls back to the chat details from messages already seen when `getChat` cannot resolve the customer.

When polling, Business mode always sends an explicit `allowed_updates` list (the default update types plus the business ones, or your `longPolling.allowedUpdates` merged with them). Telegram otherwise reuses the list from an earlier call, which can silently exclude business updates. When registering a webhook yourself, add `business_connection`, `business_message`, and `edited_business_message` to `allowed_updates`.

Business thread IDs start with `telegram:biz`, so state adapters that shard by the first two ID segments (such as [Cloudflare Agents](/adapters/vendor-official/cloudflare-agents#state-sharding)) place every business conversation in one shard. Override the sharder with a key that includes the connection ID if that matters for your deployment.

## Authentication

Create a bot via [BotFather](https://t.me/BotFather):

1. Send `/newbot` and follow the prompts.
2. Copy the token to `TELEGRAM_BOT_TOKEN`.
3. Optionally pick a username and copy it to `TELEGRAM_BOT_USERNAME`.

## Advanced

### Inbound attachments

Incoming file attachments expose a lazy `fetchData()` served from the configured Bot API host. Downloads are limited to 25 MB and time out after 30 seconds. They use the Web Fetch API, so file downloads keep working in runtimes like Cloudflare Workers.

### Polling for local development

```typescript title="lib/bot.ts" lineNumbers
import { Chat } from "chat";
import { createTelegramAdapter } from "@chat-adapter/telegram";
import { createMemoryState } from "@chat-adapter/state-memory";

const telegram = createTelegramAdapter({
  mode: "polling",
});

const bot = new Chat({
  userName: "mybot",
  adapters: { telegram },
  state: createMemoryState(),
});
```

Polling and webhooks are mutually exclusive in Telegram. `mode: "polling"` deletes the webhook by default before calling `getUpdates`.

### Auto mode

`mode: "auto"` (the default) checks `getWebhookInfo`: if a webhook URL is set, it uses webhook mode; otherwise it falls back to polling on long-running runtimes. If `getWebhookInfo` fails, the adapter stays in webhook mode (safe fallback).

```typescript
const telegram = createTelegramAdapter({ mode: "auto" });
void bot.initialize();
console.log(telegram.runtimeMode); // "webhook" | "polling"
```

### Slash commands

Use `bot.onSlashCommand` to handle Telegram bot commands such as `/status` and `/status@mybot`. Commands addressed to another bot are ignored as slash commands and continue through the normal message path.

### Streaming

Streams use post-and-edit by default for consistent behavior across Telegram clients. To opt into native draft previews in private chats:

```typescript
const telegram = createTelegramAdapter({ nativeStreaming: true });
```

[Telegram clients should dismiss a draft preview](https://core.telegram.org/api/bots/ai#live-response-streaming) when the final message arrives, but draft rendering varies between clients. Keep the default when your bot must work consistently across Telegram clients.

Telegram recommends at most one message per second in a single chat and limits groups to 20 messages per minute. Sends and edits share flood control, so the post-and-edit path defaults to 1100ms between operations in private chats and 3100ms in other chats. This is a floor: setting a lower `streamingUpdateIntervalMs` on your `Chat` instance does not push the adapter past it. Override it with `streamingEditIntervalMs`:

```typescript
const telegram = createTelegramAdapter({ streamingEditIntervalMs: 4000 });
```

If Telegram rate limits the final edit, the adapter waits and retries when the requested delay is 5 seconds or less. Longer delays and failed retries reject the post so it never reports text that Telegram did not receive.

### Markdown formatting

On Telegram Bot API 10.1 and newer, explicit `{ markdown }` and `{ ast }` messages use rich messages. Standard markdown gains native headings, lists, tables, task lists, formulas, details, and separate media blocks where supported by Telegram.

Plain strings, raw messages, cards, and media captions retain their existing lightweight message paths. Cards and captions use Telegram's `MarkdownV2` parse mode with context-aware escaping. Older or custom Bot API servers automatically fall back to this existing path when rich message methods are unavailable.

Pass `{ raw: "..." }` only if you need to ship a fully pre-escaped MarkdownV2 string.

### Notes

* Accepted webhook updates with an integer `update_id` are deduplicated for 24 hours through the configured state adapter. Use shared durable state across serverless instances. If state is unavailable, the adapter returns 503 so Telegram retries without dispatching.
* Webhook mode requires `secretToken` unless `allowUnverifiedWebhooks` is explicitly enabled. Polling mode does not require webhook verification.
* Telegram does not expose full historical message APIs to bots. `fetchMessages` returns adapter-cached messages from the current process.
* `listThreads` is not available for Telegram chats.
* Telegram callback data is limited to 64 bytes — keep `Button` `id`/`value` payloads short.
* Incoming attachments preserve Telegram's downloadable `file_id` and stable `file_unique_id` as `fetchMetadata.fileId` and `fetchMetadata.fileUniqueId`. Photo attachments use the `image/jpeg` MIME type.
* Multiple `files` or compatible `attachments` are sent as Telegram media groups. `files` upload as documents; `attachments` preserve image, audio, video, or file media type.
* Incoming media groups are delivered as one message after the album settles, with attachments ordered by Telegram message ID and the shared caption preserved.
* Other rich card elements (images, select menus, radios) render as fallback text.
* `Thread.reply()` threads with Bot API `reply_parameters` and sets `allow_sending_without_reply`, so a reply whose target was deleted, never existed, or lives in another forum topic is delivered unthreaded instead of failing. Only the chat half of a composite target id is validated up front.

## Feature support

<FeatureSupport />
