Telegram Rich Message Builder
A Telegram rich message is a message assembled from structured blocks rather than from one formatted string. Bot API 10.1 introduced them on 11 June 2026 and gave bots headings, lists, tables, collapsible sections, pull quotes, formulas, maps and collages, with the text ceiling raised from 4,096 to 32,768 characters; 10.2 added the input side of every block, and 10.3 added buttons inside the message body. The difficulty did not disappear with the old escaping rules — it moved. Instead of one string to escape you now assemble a tree of twenty-four block types, each with its own required fields, under five limits that only announce themselves as a rejection. This builder turns Markdown into that tree, checks it against every documented rule, and hands you a send call that runs.
- All 24 block types. Everything in 10.1, 10.2 and 10.3, each with a validated example you can drop straight into your message.
- A validator with a path. Paste a payload you already have and get
blocks[2].items[0]instead ofBad Request. - No token, no upload. The whole thing runs in this tab. We never ask for a bot token, here or anywhere else on the site.
The limits nobody finds until the API says no
- 500
- blocks, counting nested blocks, list items and table rows. A 400-item list is 801 blocks, not one — the counter above shows the real number.
- 32,768
- characters, measured in UTF-8. Non-Latin text reaches it at roughly half the character count you would expect.
- 16
- levels of nesting, across both formatting and blocks.
- 20
- columns in a table, counting
colspan. - 50
- media attachments per message.
Write it as Markdown
Rich messages are the first Telegram format where Markdown maps onto something real: a heading stays a heading, a table stays a table. Paste what you have.
How it will look
Approximate — clients differ in spacing — but structurally what the API produces.
Weekly report
Deployments finished without a rollback for the first time this quarter.
| Service | p95 | Errors |
|---|---|---|
| api | 41ms | 0.02% |
| web | 88ms | 0.11% |
- ☑
Migration applied
- ☑
Cache warmed
- ☐
Load test re-run
bashThe one incident was a config change, as usual.
kubectl rollout status deploy/apiFull numbers in the dashboard.
Blocks 18 of 500 · 280 of 32768 characters · depth 2 of 16
- Section heading
heading - Paragraph
paragraph - Table
table3 rows × 3
- List
list3 items
- Block quotation
blockquote1 nested block
- Code block
pre - Divider
dividerNo editable text — change it in the Markdown.
- Paragraph
paragraph
Valid against every rule and limit in the specification.
What to send
{
"blocks": [
{
"type": "heading",
"text": "Weekly report",
"size": 1
},
{
"type": "paragraph",
"text": [
"Deployments finished ",
{
"type": "bold",
"text": "without a rollback"
},
" for the first time this quarter."
]
},
{
"type": "table",
"cells": [
[
{
"text": "Service",
"is_header": true,
"align": "left"
},
{
"text": "p95",
"is_header": true,
"align": "right"
},
{
"text": "Errors",
"is_header": true,
"align": "right"
}
],
[
{
"text": "api",
"align": "left"
},
{
"text": "41ms",
"align": "right"
},
{
"text": "0.02%",
"align": "right"
}
],
[
{
"text": "web",
"align": "left"
},
{
"text": "88ms",
"align": "right"
},
{
"text": "0.11%",
"align": "right"
}
]
],
"is_bordered": true
},
{
"type": "list",
"items": [
{
"blocks": [
{
"type": "paragraph",
"text": "Migration applied"
}
],
"has_checkbox": true,
"is_checked": true
},
{
"blocks": [
{
"type": "paragraph",
"text": "Cache warmed"
}
],
"has_checkbox": true,
"is_checked": true
},
{
"blocks": [
{
"type": "paragraph",
"text": "Load test re-run"
}
],
"has_checkbox": true
}
]
},
{
"type": "blockquote",
"blocks": [
{
"type": "paragraph",
"text": "The one incident was a config change, as usual."
}
]
},
{
"type": "pre",
"text": "kubectl rollout status deploy/api",
"language": "bash"
},
{
"type": "divider"
},
{
"type": "paragraph",
"text": "Full numbers in the dashboard."
}
]
}What actually changed in 10.1
The old problem was a string: eighteen reserved characters, different rules inside code blocks and link targets, and an error message that named a byte offset. The new problem is a tree. A heading is { type: "heading", text, size }, where size runs from 1 for the largest to 6 for the smallest; a list is items that each hold their own array of blocks, so lists nest and can contain anything; a table is rows of cells with headers, spans and alignment. That is more expressive and considerably more tedious to assemble by hand, which is why the first thing this page does is build the tree from Markdown you already have.
Three pairs of fields that look interchangeable and are not
An ordinary quotation takes blocks; the expandable one takes text. A button’s copy_text is an object with a text field, not a string, and disabled is an empty object rather than true. Heading size counts down, not up, so 1 is the biggest. Every one of these is a reasonable guess that the API refuses, and each is called out by name in the validator rather than left as a generic complaint.
Streaming a model’s answer
sendRichMessageDraft exists for bots that answer with a language model. It streams a partial message that lives about thirty seconds and animates into the next draft with the same draft_id, which is what makes the answer appear to arrive as it is written. Two things are easy to miss. The draft does not persist, so the finished message still has to be sent with sendRichMessage. And the thinking block is valid only in a draft — putting it in the final message is a rejection.
Rich messages — questions
- What is a Telegram rich message?
- A message built from structured blocks instead of one formatted string. Bot API 10.1, released on 11 June 2026, added headings, lists, tables, collapsible sections, pull quotes, formulas, collages and more, and raised the text ceiling from 4,096 to 32,768 characters. 10.2 added the input side of every block type, and 10.3 added buttons inside the message body, a document block and expandable quotations. You send one with sendRichMessage, passing an InputRichMessage object in the rich_message parameter.
- How do I send a rich message with a bot?
- Call sendRichMessage with chat_id and rich_message. That object holds exactly one of three fields: blocks (an array of block objects), html (a Rich HTML string), or markdown. Building the array gives the most control; the html field is the practical choice while your library has no typed support yet, because it is a single string you can put in a raw request. Build the payload above and copy the call for your language.
- Which libraries support Bot API 10.1 rich messages?
- Coverage is still patchy. Checked on 6 September 2026: python-telegram-bot issue #5261, "Full Support for Bot API 10.1", is open with no merged pull request and its milestone unreleased, so people are sending these payloads through its raw request layer as plain dictionaries. Several agent frameworks have open requests of their own. The practical consequence is that a snippet calling a typed send_rich_message would not run for most people today, which is why the ones here go through each library's raw request layer instead — those work now and keep working after the wrapper lands. This is the fastest-moving fact on the page; check the issue before you assume it still holds.
- What are the limits on a rich message?
- Five of them, and none announces itself until the API rejects your payload: 32,768 UTF-8 characters including custom emoji alternative text and formula source; 500 blocks in total, counting nested blocks, list items, ordered list items, table rows, quotation blocks and details blocks; 16 levels of nesting; 50 media attachments; and 20 columns in a table. The builder above counts all five as you work, which matters most for the block count — a list of 400 items is 801 blocks, not one.
- Can I convert Markdown into rich message blocks?
- Yes, and this is the first Telegram format where that conversion is not lossy. In a classic message a heading has to become bold text and a table has to become a monospace block, because Telegram has nothing else to map them to. Rich messages have a real heading block with six sizes, a real list with checkbox and numbering support, and a real table with header cells, spans and alignment. Paste Markdown above and the tree is built from it directly.
- What is sendRichMessageDraft for?
- Streaming a partial message while it is still being generated — the case that appears the moment a bot is answering with a language model. The draft is ephemeral: it acts as a roughly 30-second preview and does not persist, so once the output is final you still call sendRichMessage with the complete message. Drafts with the same draft_id animate into one another, which is what produces the appearance of text arriving as it is written. The thinking block, which shows a "Thinking…" placeholder, is valid only in a draft and will be rejected by sendRichMessage.
- Why does the API reject my copy_text button?
- Almost certainly because it was passed as a string. In the specification copy_text is a CopyTextButton object, so it needs { "text": "..." } rather than "...". The neighbouring trap is disabled, which is a DisabledButton — an object that currently holds no fields — so it takes {} and not true. Both look like reasonable guesses and both are rejected. The validator above flags each of them by name.
- Is my payload sent anywhere?
- No. The conversion, validation and preview all run in this browser tab; nothing is uploaded and no bot token is ever requested. A builder cannot send your message for you without your token, and there is no version of that we would rather have than you copying a call and running it yourself.
Still on classic messages? The formatter handles MarkdownV2, HTML and entities, with the same parser behind it. The rest of the set is on the tools hub.