Skip to content
Back to skills

Telegram Rich Messages

ASecurity

Send Telegram Rich Messages via Bot API 10.x sendRichMessage: collapsible expandable blockquotes, details blocks, tables, checklists, code blocks, spoilers, date widgets, dividers, footers — all in ONE message. Contains LIVE-VERIFIED JSON shapes (many differ from the official docs!), a ready Python sender, a probe script, and the troubleshooting table of API error messages. Use when posting changelogs, reports, digests, or any formatted content to Telegram chats/threads/topics. Triggers: rich...

  • 15 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
documentationpythonrustgonodegitapi

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add youlianvr/oper-share --skill telegram-rich-messages --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Telegram Rich Messages?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Telegram Rich Messages
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/youlianvr-telegram-rich-messages/badge)](https://www.skillsdirectory.com/skills/youlianvr-telegram-rich-messages)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: telegram-rich-messages
description: >
  Send Telegram Rich Messages via Bot API 10.x sendRichMessage: collapsible
  expandable blockquotes, details blocks, tables, checklists, code blocks,
  spoilers, date widgets, dividers, footers — all in ONE message. Contains
  LIVE-VERIFIED JSON shapes (many differ from the official docs!), a ready
  Python sender, a probe script, and the troubleshooting table of API error
  messages. Use when posting changelogs, reports, digests, or any formatted
  content to Telegram chats/threads/topics. Triggers: rich message,
  sendRichMessage, свёрнутый блок, collapsible block, expandable blockquote,
  TG post, changelog to Telegram, telegram formatting.
metadata:
  bot-api: "10.3 (2026-08-24)"
  verified: "2026-09-03, live probes against api.telegram.org"
---

# Telegram Rich Messages (Bot API 10.x)

Rich Messages are structured messages built from BLOCKS (not parse_mode
markup). One message can contain text, collapsible sections, tables,
checklists, code blocks, media, and (since 10.3) buttons. Sent via the
`sendRichMessage` method.

## Method

```
POST https://api.telegram.org/bot<TOKEN>/sendRichMessage
{
  "chat_id": <id>,
  "message_thread_id": <topic_id>,          // optional, for forum topics
  "rich_message": { "blocks": [ ...blocks ] }
}
```

Requires a Bot API 10.3+ server. If the token's server predates 10.x you
get `Method not found` — fall back to classic `sendMessage` with
`parse_mode: "HTML"` and `<blockquote expandable>…</blockquote>`.

## ⚠️ LIVE-VERIFIED SHAPES (docs diverge — trust these)

Field-name traps found by probing the real API. The official changelog
names classes (e.g. `InputRichBlockExpandableBlockQuotation`) but does NOT
show the JSON wire format; several intuitive shapes FAIL.

### Blocks — supported `type` values (verified OK)

| type | fields | notes |
|---|---|---|
| `paragraph` | `text`: RichText | base text block |
| `expandable_blockquote` | `text`: RichText, `credit`?: RichText | **collapsible** quote, collapsed by default; field is `text` (RichText), NOT `children`/`blocks` |
| `blockquote` | `blocks`: Block[], `credit`?: RichText | regular quote; field is `blocks` (array of blocks), NOT `text` — `text` → `RICH_MESSAGE_EMPTY` |
| `details` | `summary`: RichText, `blocks`: Block[], `is_open`?: true | collapsible section (like `<details>`); content in `blocks` |
| `pre` | `text`: str, `language`?: str | code block. Type is `pre`, NOT `preformatted`/`code_block` |
| `list` | `items`: ListItem[] | see ListItem below; items are objects, NOT strings |
| `table` | `cells`: Cell[][] | 2D array of cells, NO row wrapper object |
| `divider` | — | horizontal rule |
| `footer` | `text`: RichText | small bottom line |

### Unsupported via sendRichMessage (probed, rejected)

`section_heading`, `heading*`, `preformatted`, `pull_quotation`,
`block_quotation` (use `blockquote`), `quotation`. Headings inside blocks:
use `paragraph` with a `bold` run — the client renders message headings
only from user-entered text.

### RichText — the `text` payload

`RichText` = plain **string** OR **array** mixing:
- plain strings,
- run objects: `{"type": "bold"|"italic"|"underline"|"strikethrough"|"marked"|"code"|"spoiler", "text": RichText}`,
- `{"type": "url", "url": "https://…", "text": RichText?}` — link (text label optional),
- `{"type": "date_time", "unix_time": 1788450000}` — date widget (field is `unix_time`, NOT `date_time`).

Gotcha: `{"type": "text", "text": "…"}` as a RUN is INVALID ("Unsupported
rich text type"). Plain text inside arrays must be a raw JSON string.

### ListItem (inside `list.items`)

```json
{
  "blocks": [ {"type": "paragraph", "text": "item text"} ],
  "has_checkbox": true,     // optional; omit for plain bullet
  "is_checked": true,       // only if has_checkbox
  "type": "1",              // optional ordered marker: "1"|"a"|"A"|"i"|"I"
  "value": 1                // optional number for ordered lists
}
```

Item content lives in `blocks` — a `text` field on the item fails with
"invalid list item type specified".

### TableCell (inside `table.cells[[][]]`)

```json
{"text": RichText?, "is_header": true?, "colspan": 2?, "rowspan": 2?,
 "align": "left"|"center"|"right", "valign": "top"|"middle"|"bottom"}
```

`align`/`valign` are required (defaults untested — set them explicitly).
Rows are plain arrays of cells: `cells: [[cell, cell], [cell, cell]]`.

## Minimal working examples

### Changelog with collapsible sections (the common case)

```python
import json, os, urllib.request
TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]

def send(blocks, chat_id, thread_id=None):
    p = {"chat_id": chat_id, "rich_message": {"blocks": blocks}}
    if thread_id: p["message_thread_id"] = thread_id
    req = urllib.request.Request(
        f"https://api.telegram.org/bot{TOKEN}/sendRichMessage",
        data=json.dumps(p).encode(), headers={"Content-Type": "application/json"})
    return json.loads(urllib.request.urlopen(req, timeout=30).read())

blocks = [
    {"type": "paragraph", "text": [{"type": "bold", "text": "🚀 Release 1.2.3"}]},
    {"type": "paragraph", "text": "Summary line."},
    {"type": "expandable_blockquote", "text": [
        {"type": "bold", "text": "✨ Features\n"},
        "• feature one\n• feature two"]},
    {"type": "expandable_blockquote", "text": [
        {"type": "bold", "text": "🛠 Fixes\n"},
        "• fix one"]},
]
print(send(blocks, chat_id=<chat-id>))
```

### RichText builder helpers (recommended)

```python
def bold(t):   return {"type": "bold", "text": t}
def ital(t):   return {"type": "italic", "text": t}
def code(t):   return {"type": "code", "text": t}
def spoil(t):  return {"type": "spoiler", "text": t}
def link(url, label=None):
    d = {"type": "url", "url": url}
    if label: d["text"] = label
    return d

def para(text): return {"type": "paragraph", "text": text}
def expq(text): return {"type": "expandable_blockquote", "text": text}   # collapsible
def quote(blocks): return {"type": "blockquote", "blocks": blocks}
def details(summary, blocks, open_=False):
    d = {"type": "details", "summary": summary, "blocks": blocks}
    if open_: d["is_open"] = True
    return d
```

## Editing & fallbacks

- Edit later: `editMessageText` accepts `rich_message` parameter (10.1+).
- Stream drafts: `sendRichMessageDraft` (typing-like streaming of blocks).
- Buttons in-message: `RichBlockButtons` / `RichTextButton` (10.3) — NOT yet
  probed; expect the same JSON-shape trial-and-error, probe first.
- Media inside message: `InputRichBlockPhoto` etc. (10.2 `blocks` variants) —
  not probed here; same advice.
- If `sendRichMessage` → `Method not found` or "unsupported": fall back to
  `sendMessage` with `parse_mode: "HTML"`; `<blockquote expandable>` gives
  the collapsible effect (no tables/checklists though).

## Troubleshooting (live-mapped error → cause)

| API error | cause |
|---|---|
| `type "X" is unsupported` | block type not in the supported table (e.g. `preformatted`, `pull_quotation`, `section_heading`) |
| `Unsupported rich text type` | a run object with unknown `type` — usually `{"type":"text",...}` inside a RichText array; use a plain string |
| `RICH_MESSAGE_EMPTY` | block content in wrong field: `blockquote` needs `blocks`, not `text`; `details` needs non-empty `blocks` |
| `RICH_MESSAGE_CONTENT_REQUIRED` | block content in wrong field: `expandable_blockquote` needs `text`, not `blocks`/`children` |
| `invalid list item type specified` | list item objects missing `blocks` or using `text` instead |
| `Can't find field "cells"` | table rows wrapped in objects; `cells` must be a 2D array of cell objects directly |
| `Can't find field "unix_time"` | date run uses `unix_time`, not `date_time`/`timestamp` |
| `Can't parse InputRichBlockListItem: Object expected` | list items as plain strings; must be objects |
| `Method not found` | server/token predates Bot API 10.x — use HTML fallback |
| `Bad Request: message thread not found` | sending to the GENERAL topic with an explicit id: General (even renamed, root id 1) REJECTS `message_thread_id: 1` — omit `message_thread_id` entirely and the message lands in General. For every other topic `message_thread_id` = ROOT MESSAGE id; userbot `list_topics` returns exactly those ids (ids are re-read with `list_topics` immediately before sending — they are not stable across conversations) |
| post lands in the WRONG topic (e.g. a changelog posted twice outside General) | wrong id→topic MAPPING on the sender side, not API misrouting — the API routes exactly to the id given (an id assumed from conversation context belonged to a different topic → duplicate posts). Resolve the topic id via `list_topics` (or the first number in `t.me/<group>/<root>/<msg>` of a message inside the TARGET topic) immediately before sending, and verify actual placement afterwards |
| `deleteMessage` returns ok but message survives | Bot API may report success in forum topics without deleting (2026-09-03: `ok: true` on msg 9396, message stayed). Verify via `forwardMessage`/`getMessages` (both honestly say "not found" vs "found") or userbot `get_message_context`; reliable removal = admin delete from a user account |
| `can't parse InputRichBlock: …` | generic: re-check field names against the tables above |

## Workspace scripts

- `a showcase script of your own` — full showcase of every verified block, sends
  to a private chat; run it to see the rendering (or forward its result).
  **Token selection:** the workspace `.env` token and the bot-MCP token are
  DIFFERENT bots. `TELEGRAM_BOT_TOKEN` in `.env` = `<bot-in-env>`;
  `mcp.json → telegram-mcp-bot.env.TELEGRAM_BOT_TOKEN` = `<bot-in-mcp-config>`
  (the one added to No AGI Chat). When posting as the group's bot, load the
  token from `%USERPROFILE%/.agents/mcp.json`, not from `.env`:
  ```python
  tok = json.load(open(r"%USERPROFILE%/.agents/mcp.json"))["mcpServers"]\
      ["telegram-mcp-bot"]["env"]["TELEGRAM_BOT_TOKEN"]
  ```
- ~~`the one-off changelog poster`~~ — removed 2026-09-03 (one-off 0.0.86
  changelog poster, recycled); use `tgsend.py` instead.
- `your Telegram sender` — **PRIMARY entry point (2026-09-03):** the general
  Telegram sender. ANY content -> NAMED targets from a local targets file you keep
  (`<channel-target>`, `<owner-dm-target>`; add new
  targets there, never hardcode ids) OR any custom chat id: bare number,
  or `c:<id>` channel/group -> -100<id>, `g:<id>` group -> -<id>,
  `u:<id>` user. Wraps rich_poster machinery:
  markdown (file via `@path`, stdin via `-`, or inline) -> rich message;
  `--photo`, `--raw-html`, `--plain`; `--dry-run`; same dedupe cache and
  forum guard. Usage:
  `python your Telegram sender @changelog.md --to freebuff-channel --footer "..."`
  `python your Telegram sender "done" --to owner-dm`
  `python your Telegram sender "hi" --to c:<chat-id>`
- ~~`the old probe scripts`~~ — recycled 2026-09-27 (six near-identical session
  probes with no caller). To test a new block type without sending anything:
  `python your rich-message poster post.md --chat <id> --dry-run`.
- `your rich-message poster` — REUSABLE poster (library layer under tgsend): markdown in -> one rich
  message out, `##` sections auto-become collapsible expandable_blockquotes
  (`--mode details` / `--mode flat`), inline md (bold/italic/code/spoiler/
  strike/underline/links), lists/checklists, GFM tables, fenced code,
  quotes, dividers, footer; HTML fallback + forum guard + DUPLICATE-POST
  GUARD: sha256(chat, thread/general, mode, blocks-JSON) is remembered in
  `a local dedupe cache file` (git-ignored, TTL 72 h) and a re-send
  of the same payload is refused with exit 1 (`--force` overrides — e.g.
  after manual deletion of the earlier post). Verified live 2026-09-03:
  2nd run refused, `--force` sent, test messages cleaned up.
  Usage: `python your rich-message poster post.md --chat <id> --thread <root_id>`
  (`--dry-run` prints JSON + HTML preview; `-` reads stdin; `--general` is
  the explicit opt-in for posting into a forum's General topic, which must
  be sent WITHOUT `message_thread_id`).

## Reading Rich Messages back

Bot API `getUpdates` delivers `rich_message` only while unconfirmed; most
MCP wrappers show `[unsupported message type]`. To read rich messages from
history, use MTProto (Telethon): the message object has `.rich_message`
with `.blocks` of `PageBlock*` nodes (`PageBlockParagraph`, `PageBlockList`,
`PageBlockPreformatted`, `PageBlockDetails`, …) and `TextPlain`/`TextConcat`
runs. Workspace session: the host's own session store, outside this repository
(`TELEGRAM_SESSION_STRING`) — see a rendered example of your own for a worked example
of rendering those blocks to markdown.

## Version note

Verified against Bot API 10.3 (2026-08-24) on 2026-09-03. Newer Bot API
versions may add block types or relax field names — re-probe before
trusting this file blindly after a major Telegram announcement.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.