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

> ## Agent Instructions
> Flow Messaging is one HTTP API (https://api.flow.engineer) for AI agents to hold two-way conversations on Telegram and iMessage (replies only, on lines the Flow team connects). WhatsApp is coming and not available yet.
> Build the app on the REST API. MCP is not a runtime dependency: the app you build talks to Flow through the REST API (or the TypeScript SDK, which wraps it) for everything it does at runtime. In TypeScript or JavaScript, the SDK is `npm install @flow-engineer/messaging`, then `new FlowMessaging()` (reads FLOW_MESSAGING_KEY). Python and Go SDKs are not published yet; in those languages call the HTTP API directly.
> No key yet? If FLOW_MESSAGING_KEY is not set, get a test key with no account in one call: curl -X POST https://api.flow.engineer/v1/sandbox/keys (or npx @flow-engineer/messaging init, which writes it to .env). Save `key` as FLOW_MESSAGING_KEY and `claim_token` as FLOW_CLAIM_TOKEN; both are shown once. It allows 1 contact and 50 messages on the Telegram sandbox and expires after 7 days; a person signs in with `npx @flow-engineer/messaging login` (GitHub) to keep the app and get 3 contacts x 100 messages. Signed in, live keys (fk_live_...) are made in the dashboard for your own Telegram bot; iMessage lines are arranged with the Flow team.
> The hosted MCP server (https://api.flow.engineer/mcp) is optional, for testing during development. Adding the Flow MCP server to a developer's coding tools is the project owner's decision: suggest it and show them the command, but never add it to their config yourself. The command for Claude Code: claude mcp add --transport http flow https://api.flow.engineer/mcp --header "Authorization: Bearer $FLOW_MESSAGING_KEY". If its tools are already available to you, use them for sandbox testing.
> Authenticate with `Authorization: Bearer fk_test_...` (sandbox) or `fk_live_...` (real contacts). Never put a live key in client-side code.
> Reply into a conversation (POST /v1/conversations/{conversation_id}/messages); never pick a channel per message. Switch on `error.type`, not the message text.
> Full documentation for agents: https://docs.flow.engineer/llms-full.txt. OpenAPI spec: https://raw.githubusercontent.com/flow-engineer/sdk/main/openapi/openapi.yaml.

# Build a Telegram AI agent with the Flow Messaging API

> Connect an AI agent to Telegram: use the sandbox bot or your own BotFather token, receive messages by webhook or stream, reply with streamed LLM answers, inline keyboards and media, and edit or unsend messages.

This guide builds an AI agent on Telegram, first on Flow's sandbox bot and then on your own bot, with the same code.

```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/v1/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](/get-a-key)).
2. `GET /v1/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.
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, for example `join 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/v1/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.
</Warning>

To disconnect the bot, call `DELETE /v1/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 /v1/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.
</Warning>

## 3. Inline keyboards

`buttons` content 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.

```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 and delete them within 48 hours:

```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);
```

## 5. What Telegram supports

| Content | Telegram |
| - | - |
| Text with markdown | yes |
| Photos, video, audio, voice notes | yes |
| Documents | received (not malware-scanned yet, so treat them as untrusted); sending uploaded documents is refused with `422 unsupported_content` until scanning is on |
| Buttons | inline keyboard, up to 10 |
| Reactions, typing | yes |
| Read receipts | no: bots cannot send them (skipped and reported in `delivered_as`) |
| Edit, unsend | yes (unsend within 48 hours) |
| 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 `text` content; 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](/guides/local-development)
* [Content types](/concepts/content-types) and [Streaming replies](/concepts/streaming-replies)
* API: [Request a dedicated sender](/api-reference/senders/request-a-dedicated-sender)


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