---
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`, and `TELEGRAM_BOT_USERNAME` 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={{
  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:
      "Optional webhook secret. 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`.",
  },
  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 required — either via config or env var.

## 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

### 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

* Verified webhook updates with an integer `update_id` are deduplicated for 24 hours through the configured state adapter. Configure `secretToken` and use shared durable state across serverless instances. If state is unavailable, the adapter returns 503 so Telegram retries without dispatching.
* 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.

## Feature support

<FeatureSupport />
