# Telegram

Source: https://staging.postgale.com/docs/telegram

Publish text, photos, videos, documents and albums to Telegram channels and groups, edit or delete them, and run the bot's chats, buttons, reactions and commands from the inbox, through the Postgale API and MCP server.

Postgale publishes to Telegram channels and groups with its own bot (Telegram Bot API): text messages, one photo, video or document,
or an album of 2 to 10 items, with edits and deletes of what was published, plus an inbox for the bot's chats (messages, inline
buttons, quick replies, edits, deletes, reactions and bot commands). Everything is available over the [REST API](https://staging.postgale.com/docs/api)
(paths relative to `https://staging.postgale.com/api/v1`) and the [MCP server](https://staging.postgale.com/docs/mcp) (`connect_account`, `create_post`, `manage_published_post`,
`get_analytics`, `send_message`, `manage_message`, `send_typing`, `telegram_bot_commands`). Each connected channel or group is one account with platform `telegram`; its `platformAccountId` is the Telegram chat id.

## Connect a channel or group

Telegram has no sign-in for bots, so connecting works with a one-time code instead of a link:

1. In Postgale open **Dashboard → Accounts → Telegram** (or ask for a code with the API or `connect_account`). You get a code that works once and expires after 15 minutes.
2. In Telegram, add Postgale's bot (shown with the code) to your channel or group as an **administrator** with the right to post messages. In channels the right is called *Post messages*.
3. Post `/connect <code>` in the channel or group. In groups `/connect@<bot> <code>` works too. You can also post it in the channel and forward that post to the bot in a private chat.

The chat appears as a Telegram account within seconds (the dashboard waits for it; API clients get the `account.connected` webhook or see it in `GET /accounts`).
When the bot may delete messages, it removes the `/connect` message afterwards.

POST /api/v1/connect/telegram:
```bash
curl -X POST https://staging.postgale.com/api/v1/connect/telegram \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"externalId":"cust-1"}'

# {"platform":"telegram","code":"K7PQ2MXH9D","command":"/connect K7PQ2MXH9D","botUsername":"PostgaleBot","instructions":[…],"expiresAt":"…","expiresInSeconds":900}
```

`GET /connect/telegram?externalId=…` returns the same. `externalId` (your own reference) is stored on the account that the code connects.

If the bot is removed from the chat or loses the right to post, the account becomes `needs_reconnect` and `account.disconnected` is
sent (reason `bot_removed`). Making the bot an admin with the right to post again restores it; posting `/connect` with a new code
works too.

## Publish

The post content is the message, up to 4096 characters. With media it is the caption of the photo, video, document or of the first
album item, up to 1024 characters. Both limits count the visible text (after `parseMode` markup) and are checked when the post is
created. Postgale uploads the files to Telegram itself: photos up to 10 MB, videos and documents up to 50 MB. Use the `publicUrl` of
[uploaded media](https://staging.postgale.com/docs/api) or any public https URL.

POST /api/v1/posts:
```bash
curl -X POST https://staging.postgale.com/api/v1/posts \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"content":"<b>Release 2.4</b> is out: faster sync and dark mode.","platforms":[{"accountId":"acc_tg","platformSpecificData":{"parseMode":"HTML","disableLinkPreview":true}}],"publishNow":true}'
```

POST /api/v1/posts:
```bash
curl -X POST https://staging.postgale.com/api/v1/posts \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"content":"Photos from the meetup 📸","platforms":[{"accountId":"acc_tg","platformSpecificData":{"mediaUrls":["https://media.postgale.com/…/1.jpg","https://media.postgale.com/…/2.jpg","https://media.postgale.com/…/clip.mp4"],"disableNotification":true}}],"publishNow":true}'
```

| platformSpecificData | What it does |
| --- | --- |
| `mediaUrl` | One photo, video or document (public https URL) |
| `mediaUrls` | An album of 2-10 items (one URL is a single media post). Photos and videos can be mixed; documents only with documents |
| `mediaType` | text \| photo \| video \| document (default: from the URL extension: jpg/png/webp photo, mp4/mov video, anything else document) |
| `parseMode` | `HTML`, `Markdown` or `MarkdownV2` (default: plain text). Edits keep it |
| `disableNotification` | Deliver silently |
| `disableWebPagePreview` | Text posts: no link preview (`disableLinkPreview` is the same) |
| `protectContent` | Recipients cannot forward or save the message |
| `buttons` | An inline keyboard under the message, up to 10, one per row: `{type: "url", title, url}` or `{type: "postback", title, payload}` (payload ≤64 bytes, delivered as `metadata.callbackData` when tapped). Not on albums. Edits keep them |

The target's `platformPostId` is `text:<message_id>`, `media:<message_id>` or `group:<id>.<id>…` (every message of an album), and
`url` is the message's t.me link (public chats, or `t.me/c/…` for members of private supergroups and channels).

## Edit and delete

| Method | Path | What it does |
| --- | --- | --- |
| POST | `/posts/{id}/edit` | `{content}`: new text of a text post, or new caption of a media post or album (same limits, same `parseMode`) |
| POST | `/posts/{id}/unpublish` | Delete the message, or every message of an album |
| GET | `/analytics/accounts/{id}` | `followers`: the chat's member count |
| GET | `/accounts/{id}/health` | Whether the bot is still an admin that may post, the chat title and member count |

Both are queued for a person while approval applies to the API key, the organization or the account (202 `pending_approval`; MCP
`manage_published_post` returns the queued action). Telegram only lets bots delete messages from the last 48 hours.

## Inbox

Telegram conversations appear in the same inbox as Instagram, Messenger, WhatsApp and X (`GET /inbox/conversations?platform=telegram`,
MCP `list_conversations`). Bots cannot read message history, so everything is stored as it arrives through the webhook and
`POST /inbox/conversations/sync` answers `not_supported`. There is no messaging window: the bot may write in any chat it is in.

- **A connected group** is one conversation (its `participant` is the group). Members' messages are inbound and carry the sender in
  `metadata.sender` on the webhook. In a connected **channel**, posts an admin makes in Telegram are stored as outbound messages.
- **Private chats** with the bot are shared by every Postgale customer, so a private chat belongs to an account only after the person
  opens the account's **start link** (`startLink` from `GET /accounts/{id}/telegram-commands`, or MCP `telegram_bot_commands` with
  action `get`). Share it on your site or in the channel. When someone opens the start links of two accounts, the latest one wins.
  Private chats that never opened a start link are not stored.

Message ids are stored as `<chatId>:<messageId>` (`platformMessageId`). Every endpoint below also accepts the bare Telegram message id or
the Postgale message id. Attachments people send arrive as Telegram file ids (`{type, id, mimeType?, filename?, url: null}`):
Telegram's download links contain the bot token, so Postgale never hands them out.

### Send

`POST /inbox/conversations/{id}/messages` (MCP `send_message`) takes `text` (or Zernio's `message`, ≤4096) and/or one attachment
(`mediaUrl` / `attachmentUrl` or `mediaId`, with `attachmentType` image | video | audio | file). With an attachment the text is its
caption (≤1024). Add at most one of:

| Field | What the person sees |
| --- | --- |
| `buttons` | An inline keyboard, up to 3, one per row: `{type: "postback", title, payload}` or `{type: "url", title, url}` |
| `quickReplies` | A one-time keyboard under the text box, up to 13 `{title}`. A tap sends the title as the person's message |
| `replyMarkup` | Any Telegram `reply_markup`: `inline_keyboard`, `keyboard`, `remove_keyboard` or `force_reply` |

`replyTo` quotes a message (its Telegram id), and `parseMode`, `disableNotification`, `protectContent` and `disableWebPagePreview`
work like on posts. A person who blocked the bot answers 409 `recipient_unavailable`; the account stays connected.

POST /api/v1/inbox/conversations/conv_tg/messages:
```bash
curl -X POST https://staging.postgale.com/api/v1/inbox/conversations/conv_tg/messages \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"message":"Your order is ready. Pick it up today?","buttons":[{"type":"postback","title":"Yes","payload":"pickup:yes"},{"type":"postback","title":"Tomorrow","payload":"pickup:later"}]}'
```

### Edit, delete and react

| Method | Path | What it does |
| --- | --- | --- |
| PATCH | `/inbox/conversations/{id}/messages/{messageId}` | `{text?, replyMarkup?}`: edit a message the bot sent, any time (a media message's caption). `replyMarkup` (`{inline_keyboard}`) replaces the buttons; text is then optional |
| DELETE | `/inbox/conversations/{id}/messages/{messageId}` | Delete a message: the bot's own any time, others' in groups where it is an admin |
| POST | `/inbox/conversations/{id}/messages/{messageId}/reactions` | `{emoji}`: react as the bot. Telegram allows a fixed set (👍 👎 ❤ 🔥 🎉 🙏 👌 😁 🤔 …); another emoji answers 400 with the list |
| DELETE | `/inbox/conversations/{id}/messages/{messageId}/reactions` | Remove the bot's reaction |
| POST | `/inbox/conversations/{id}/typing` | Show "typing…" for about 5 seconds |
| PUT | `/inbox/conversations/{id}` | `{status: "archived"}` or `"active"` (Postgale only) |

Each answers `{message}` with the updated stored copy. Over MCP these are `manage_message` (edit, delete, react, unreact) and
`send_typing`.

Sends, edits, deletes and reactions are queued for a person while approval applies, like every other agent action.

### Bot commands

The "/" menu people see when they chat with the bot. Postgale's bot serves every customer, so commands are set per chat: in the
account's group (channels have no command menu) and in every private chat bound to the account, including ones that open the start
link later.

| Method | Path | What it does |
| --- | --- | --- |
| GET | `/accounts/{id}/telegram-commands` | `{data: [{command, description}], startLink}` |
| PUT | `/accounts/{id}/telegram-commands` | `{commands: [...]}`: replace them. Up to 100; `command` is lowercase a-z, 0-9 and `_`, at most 32 characters, no leading slash; `description` at most 256 |
| DELETE | `/accounts/{id}/telegram-commands` | Clear them |

PUT /api/v1/accounts/acc_tg/telegram-commands:
```bash
curl -X PUT https://staging.postgale.com/api/v1/accounts/acc_tg/telegram-commands \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"commands":[{"command":"order","description":"Place an order"},{"command":"help","description":"Talk to a person"}]}'
```

A command someone sends arrives as an ordinary `message.received` with the text (`/order`).

### Webhooks

| Event | When |
| --- | --- |
| `message.received` | A message in a connected group, or in a private chat bound to the account (also `/start` when the start link is opened, with `metadata.command: "start"`). A tap on an inline button is a message of type `postback` with `postback.payload` and `metadata.callbackData` |
| `message.sent` | The bot sent a message through Postgale, or an admin posted in a connected channel |
| `message.edited` | Someone edited a message (also an admin's edit of a channel post). `metadata.editHistory` lists the previous versions |
| `reaction.received` | Someone added or removed a reaction in a group or channel where the bot is an admin. One event per emoji: `reaction: {emoji, action: added \| removed, messageId, platformMessageId, sender, reactedAt}`. Telegram sends no reactions from private chats |

Telegram tells bots nothing about deleted or read messages.

## Telegram's rules to know

- **Rate limits**: when Telegram answers 429, Postgale retries the post after the `retry_after` Telegram asks for. Telegram allows about 20 messages per minute per group.
- **Rights**: in channels the bot must be an admin with *Post messages* (and *Edit messages of others* / *Delete messages* is not needed for its own posts). In groups it must be an admin.
- **Errors**: "bot was kicked", "not enough rights" or "chat not found" mark the account `needs_reconnect` (`reconnect_required` on the API).
- **Analytics**: the Bot API gives bots no views or reactions per message, so post analytics answer `not_supported`; the member count is available as an account metric.
- **Groups upgraded to supergroups** get a new chat id from Telegram; Postgale follows the change automatically.
