> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flow.engineer/llms.txt
> Use this file to discover all available pages before exploring further.

# Telegram

> Try the sandbox bot, then connect your own Telegram sender.

Try your agent on the Telegram sandbox, then connect your own bot without changing its message handler.

```ts TypeScript theme={null}
// npm install @flow-engineer/messaging openai
import OpenAI from "openai";
import { FlowMessaging, contentText } from "@flow-engineer/messaging";

const flow = new FlowMessaging();
const openai = new OpenAI();

for await (const event of flow.events.stream({ types: ["message.received"] })) {
  await event.conversation.reply(
    openai.chat.completions.create({
      model: "gpt-4.1-mini",
      stream: true,
      messages: [{ role: "user", content: contentText(event.data.message.content) }],
    }),
  );
}
```

## 1. Try it on the sandbox bot

1. Get a test key, with no account: `curl -X POST https://api.flow.engineer/v0/sandbox/keys` (or `npx @flow-engineer/messaging init`). Set its `key` as `FLOW_MESSAGING_KEY`. It allows 1 contact and 50 messages for 7 days; sign in to get more ([Keys and sign-in](https://docs.flow.engineer/unified-messaging-api/get-a-key.md)).
2. `GET /v0/senders?channel=telegram` lists the sandbox bot. Its `address.link` is `https://t.me/<bot>?start=<code>`, with your join code in it. (`npx @flow-engineer/messaging init` prints it too.)
3. Open the link and tap **Start**. That's it, you've joined: your app gets `conversation.started`, then `/start` as your first `message.received`.
4. Run the code above and send the bot a message.

If the link can't be used (for example you found the bot by searching for it), send the bot the sender's `join_code` instead, as it is, for example `Hi Sandbox app (ref wild-otter-04508705)`.

## 2. Your own Telegram bot

Telegram is the fastest channel to go live on: bring a bot token and it is connected at once.

1. In Telegram, open **@BotFather**, send `/newbot`, and copy the token.
2. Put the token in an environment variable on your server or in your terminal, and connect it with a **live** key (`FLOW_MESSAGING_KEY=fk_live_...`):

```bash curl theme={null}
export TELEGRAM_BOT_TOKEN=...   # the token from BotFather; never commit it

curl https://api.flow.engineer/v0/senders \
  -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"channel\": \"telegram\", \"telegram_bot_token\": \"$TELEGRAM_BOT_TOKEN\"}"
```

Flow checks the token, keeps it encrypted, points the bot's webhook at Flow and answers with the sender `active`. The token is never returned. With `FLOW_MESSAGING_KEY` set to the live key, your agent now answers on your bot.

> [!WARNING]
>
> The bot token controls your bot. Never paste it into a chat with an AI assistant (including your coding agent), and never commit it. Keep it in an environment variable and send it from your own server or terminal, as above.

To disconnect the bot, call `DELETE /v0/senders/{sender_id}` with your live key: Flow removes the bot's webhook, deletes its token and retires the sender (`banned`).

If Telegram rejects the token (you revoked it in @BotFather), the sender turns `flagged`: new sends answer `403 permission` and queued messages are held. Send the new token the same way (`POST /v0/senders`) and the same sender becomes `active` again and sends them. A held message waits at most 72 hours after Flow accepted it; after that it fails with `outside_window` (`channel_code` `queued_too_long`) in `message.failed` instead of going out late.

> [!WARNING]
>
> A bot can have only one webhook. Connecting a bot to Flow replaces any webhook it had, so do not keep another server polling or receiving updates for the same bot.

## 3. Inline keyboards

A [`buttons`](https://docs.flow.engineer/api-reference/messages/send-text-with-buttons) message shows as an inline keyboard (1 to 10 buttons). Taps arrive as `button_reply` with the button's `id`. URL buttons open a link and send nothing back. Telegram has no `list` or `carousel`: those answer `422 unsupported_content`, and the `hint` says what to send instead (for a carousel, a basic message, then one buttons message with an image `header` per card).

```ts TypeScript theme={null}
import { buttons } from "@flow-engineer/messaging";

await event.conversation.send(buttons("Pick a size", ["Small", "Medium", "Large"]));
```

## 4. Edit and unsend

Telegram lets bots edit their messages at any time and unsend them within 48 hours of sending:

```ts TypeScript theme={null}
const [msg] = await event.conversation.reply("Checking stock...");
await flow.messages.edit(msg.id, "In stock: 12 left.");
await flow.messages.unsend(msg.id);
```

An unsend more than 48 hours after sending is refused at once with `422 unsupported_content` and `channel_code` `unsend_window_closed`; send a new message instead. A `200` means accepted: wait for `message.sent` (or `message.failed`) for the unsend. See [Edit and unsend limits](https://docs.flow.engineer/unified-messaging-api/concepts/content-types.md#edit-and-unsend-limits).

## 5. What Telegram supports

| Kind | Telegram |
| - | - |
| Text (`basic`) | yes, sent as written; style it with Telegram's own HTML or MarkdownV2 in `telegram.parse_mode` |
| Photos, video, audio, voice notes | yes |
| Documents | yes, sent (up to 50 MB) and received; malware-scanned before Flow keeps them (an upload that fails answers `422 file_blocked`) |
| Buttons | inline keyboard, up to 10; no lists or carousels |
| Reactions, typing | yes; reactions only with Telegram's reaction emoji |
| Read receipts | no: bots cannot send them (`422 unsupported_content`, `param` `kind`) |
| Edit, unsend | yes: edits with no time limit, unsends within 48 hours (otherwise `422 unsupported_content`, `unsend_window_closed`) |
| Starting a conversation | only with people who started your bot (`to.telegram_user_id`) |

## 6. Telegram rules worth knowing

* **People must start your bot first.** Telegram does not let a bot message someone who has never pressed Start.
* **Commands are messages.** `/start` and other commands arrive as `basic` messages; your agent decides what to answer.
* **Contacts have no phone number.** A Telegram contact is identified by `address.telegram_user_id` and maybe `address.username`.

## Related

* [Local development](https://docs.flow.engineer/unified-messaging-api/guides/local-development.md)
* [Content types](https://docs.flow.engineer/unified-messaging-api/concepts/content-types.md) and [Streaming replies](https://docs.flow.engineer/unified-messaging-api/concepts/streaming-replies.md)
* API: [Request a dedicated sender](https://docs.flow.engineer/api-reference/senders/request-a-dedicated-sender.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.