Telegram markdown (MarkdownV2) cheat sheet

Updated

Telegram has three different formatting syntaxes: the shortcuts you type in the app (**bold**, __italic__), the Bot API's MarkdownV2 (*bold*, _italic_, 18 characters that must be backslash-escaped) and the Bot API's HTML mode. None of them is GitHub markdown: there are no headings, lists, tables or images. Every snippet below is taken from the Bot API formatting options and Telegram's own app strings.

Telegram formatting at a glance

EffectApp shortcutMarkdownV2HTMLNotes
Bold**bold***bold*<b>bold</b> or <strong>MarkdownV2 uses a single asterisk.
Italic__italic___italic_<i>italic</i> or <em>In MarkdownV2 double underscore means underline, not italic.
Underlinemenu only__underline__<u>underline</u> or <ins>
Strikethrough~~strike~~~strike~<s>strike</s>, <strike>, <del>MarkdownV2 uses a single tilde.
Spoiler||spoiler||||spoiler||<span class="tg-spoiler">…</span> or <tg-spoiler>
Inline code`mono``mono`<code>mono</code>
Code blockmenu only```lang code ```<pre><code class="language-lang">code</code></pre>
Link[text](url)[text](https://example.com/)<a href="https://example.com/">text</a>Inside the (…) part, ) and \ must be escaped.
Mention by id—[name](tg://user?id=123456789)<a href="tg://user?id=123456789">name</a>
Quotemenu only>line one >line two<blockquote>…</blockquote>Every quoted line starts with >.
Expandable quotemenu only**>first line >last line||<blockquote expandable>…</blockquote>
Custom emojipremium picker![👍](tg://emoji?id=5368324170671202286)<tg-emoji emoji-id="5368324170671202286">👍</tg-emoji>Bots need a Fragment username or a Premium owner to send these.

Formatting in the Telegram app

Telegram apps (iOS, Android, Desktop, Web) format text through the selection menu, and also recognise a handful of typed shortcuts. The shortcuts are listed in Telegram’s own interface strings; they are not the same as the Bot API syntax further down.

Typed shortcuts the app converts as you send

**bold** __italic__ ~~strikethrough~~ `monospace` ||spoiler|| [text](https://example.com)

bold italic strikethrough monospace spoiler (hidden until tapped) text

Double underscores are italic in the app but underline in Bot API MarkdownV2 — the single most common mix-up when people move from chatting to bot code.

Only in the menu

Select text → Underline, Quote, Spoiler, Code block (with language), Link

There is no typed shortcut for underline, quotes or language-tagged code blocks in the app; select the text and use the formatting menu.

Bot API: MarkdownV2

Pass parse_mode = "MarkdownV2" with sendMessage. Entities can be nested (bold inside italic inside spoiler), but pre/code cannot contain other entities and blockquotes cannot be nested.

Inline styles

*bold* _italic_ __underline__ ~strikethrough~ ||spoiler|| `inline code`

bold italic <u>underline</u> strikethrough spoiler inline code

Single asterisk is bold, single underscore is italic, double underscore is underline, single tilde is strikethrough — four of these differ from GitHub markdown.

Nesting

*bold _italic bold ~italic bold strikethrough ||italic bold strikethrough spoiler||~ __underline italic bold___ bold*

The official example from the Bot API docs; note the ___ at the end is underline-close plus italic-close, which works only because underline is parsed greedily left to right.

Italic + underline needs an empty bold separator

___italic underline_**__

Because __ is always read as underline, ___text___ is ambiguous; the docs tell you to insert an empty bold entity (**) between the closing _ and __.

Links and mentions

[inline URL](http://www.example.com/)
[inline mention of a user](tg://user?id=123456789)

Inside the (...) part, every ) and \ must be escaped; tg://user?id= links only work in inline links and inline keyboard buttons.

Code and pre blocks

`inline fixed-width code`
```
pre-formatted fixed-width code block
```
```python
print("hello")
```

inline fixed-width code

pre-formatted fixed-width code block
print("hello")

Inside pre and code entities, all ` and \ characters must be escaped; nothing else needs escaping there.

Block quotation

>Block quotation started
>Block quotation continued
>The last line of the block quotation

Block quotation started Block quotation continued The last line of the block quotation

Each line of the quote starts with >; a > anywhere else in the message must be escaped as \>.

Expandable block quotation

**>The expandable block quotation started
>Expandable block quotation continued
>Hidden by default part
>The last line with the expandability mark||

Starts with an empty bold entity (**) before the first >, and ends with || on the last line; clients show the first lines and a "show more" control.

Escaping reserved characters

Order \#42 costs 9\.99 \(incl\. VAT\) \- ships 2026\-10\-15\!

Any character with code 1–126 can be escaped with a preceding backslash; the 18 characters in the table below MUST be escaped whenever they are not part of markup.

Bot API: HTML parse mode

Pass parse_mode = "HTML". Only the tags below are supported, and every <, > and & that is not part of a tag must become &lt;, &gt; and &amp;. Only four named entities are recognised: &lt; &gt; &amp; &quot; (numeric entities all work).

All supported tags

<b>bold</b> <strong>bold</strong>
<i>italic</i> <em>italic</em>
<u>underline</u> <ins>underline</ins>
<s>strike</s> <strike>strike</strike> <del>strike</del>
<span class="tg-spoiler">spoiler</span> <tg-spoiler>spoiler</tg-spoiler>
<a href="http://www.example.com/">inline URL</a>
<a href="tg://user?id=123456789">inline mention of a user</a>
<tg-emoji emoji-id="5368324170671202286">👍</tg-emoji>
<code>inline fixed-width code</code>
<pre>pre-formatted fixed-width code block</pre>
<pre><code class="language-python">pre-formatted code in Python</code></pre>
<blockquote>Block quotation</blockquote>
<blockquote expandable>Expandable block quotation</blockquote>

A language can only be set with nested <pre><code class="language-xxx">; a standalone <code> tag cannot carry a language.

Escaping user text for HTML mode

if a &lt; b &amp;&amp; b &gt; c

Three replacements (& first, then < and >) are all that is needed, which is why HTML mode is the safer choice for programmatically built messages.

Legacy Markdown (parse_mode = "Markdown")

Kept for backward compatibility. Same bold/italic/code/link syntax as MarkdownV2 but no underline, strikethrough, spoiler, blockquote, custom emoji — and no nesting.

Everything legacy Markdown can do

*bold text*
_italic text_
[inline URL](http://www.example.com/)
[inline mention of a user](tg://user?id=123456789)
`inline fixed-width code`
```python
pre-formatted fixed-width code block
```

bold text italic text inline URL inline mention of a user inline fixed-width code

pre-formatted fixed-width code block

Only _ * ` [ need escaping outside entities; escaping inside an entity is not allowed, so for italic snake_case the docs say to close and reopen: _snake_\__case_.

MarkdownV2 escape table

From the Bot API docs: "In all other places characters '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' must be escaped with the preceding character '\'."

CharacterWriteWhere it must be escaped
_\_Everywhere outside markup
*\*Everywhere outside markup
[\[Everywhere outside markup
]\]Everywhere outside markup
(\(Everywhere outside markup
)\)Everywhere outside markup; also inside a link URL
~\~Everywhere outside markup
`\`Everywhere outside markup; also inside pre and code
>\>Everywhere outside markup
#\#Everywhere outside markup
+\+Everywhere outside markup
-\-Everywhere outside markup
=\=Everywhere outside markup
|\|Everywhere outside markup
{\{Everywhere outside markup
}\}Everywhere outside markup
.\.Everywhere outside markup
!\!Everywhere outside markup
\\\Everywhere (it is the escape character itself); also inside pre, code and link URLs

A minimal escape function replaces each of those characters with a backslash plus the character, in one regular expression, before you add any markup: text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&').

Why does my Telegram bot return "Bad Request: can't parse entities"?

In MarkdownV2 every reserved character that is not part of your markup must be escaped with a backslash: _ * [ ] ( ) ~ ` > # + - = | { } . ! — including the period at the end of a sentence and the hyphen in a date. One unescaped "." or "-" is enough to make the whole sendMessage call fail. Escape the text first, then wrap it in markup, or switch parse_mode to HTML, which only needs <, > and & escaped.

What is the difference between Markdown and MarkdownV2 in the Telegram Bot API?

Markdown is the legacy mode: single asterisks for bold, underscores for italic, backticks for code, [text](url) links, no nesting and no underline, strikethrough, spoiler or blockquote. MarkdownV2 adds __underline__, ~strikethrough~, ||spoiler||, >quotes, custom emoji and nested entities, but requires escaping of 18 reserved characters. The Bot API documentation calls Markdown "a legacy mode, retained for backward compatibility".

How do I make text bold, italic or monospace in the Telegram app?

Select the text and use the formatting menu (Bold, Italic, Underline, Strikethrough, Spoiler, Mono, Link, Quote), or type the shortcuts Telegram lists in its own interface strings: **bold**, __italic__, ~~strikethrough~~, `monospace`, ||spoiler|| and [text](url). Note that the app shortcuts are not the same as Bot API MarkdownV2 — the app uses double underscores for italic, while MarkdownV2 uses a single underscore for italic and double underscores for underline.

Should I use MarkdownV2 or HTML for a Telegram bot?

HTML is usually easier for generated text: only <, > and & need escaping (as &lt;, &gt;, &amp;), the tag set is small (b, i, u, s, span class="tg-spoiler", a, code, pre, blockquote, tg-emoji), and the same entities are available as in MarkdownV2. MarkdownV2 is shorter to type by hand but any user-supplied text must be run through an escape function first.

How do I write a code block with a language in a Telegram message?

In MarkdownV2 and legacy Markdown, open a fence with three backticks followed by the language name on the same line (```python), put the code on the following lines, and close with three backticks. In HTML, nest <pre><code class="language-python">…</code></pre>. Inside pre and code entities, backticks and backslashes must be escaped with a backslash.

How do I make an expandable (collapsed) quote in Telegram?

In MarkdownV2, start the first line of the quote with **> (an empty bold entity followed by the quote marker) and end the last line with ||. In HTML use <blockquote expandable>…</blockquote>. Regular quotes are lines starting with > in MarkdownV2 or a <blockquote> tag in HTML. Blockquotes cannot be nested.

Does Telegram support headings, tables or bullet lists?

No. Telegram messages have no heading, table, list or image-embed entities in any parse mode. Typing # Heading or - item sends the characters literally (and in MarkdownV2 the # and - must be escaped). Use bold for titles, plain bullets like • for lists, and a <pre> block for anything column-aligned.

Standard markdown cheat sheet →

Related

Markdown cheat sheet · WhatsApp formatting · Discord markdown · Slack formatting · Escape characters guide · Code block guide · Blockquote guide