Fix Telegram's “Bad Request: can't parse entities” error
You call sendMessage with parse_mode: "MarkdownV2" and Telegram answers:
400 Bad Request: can't parse entities: Character '.' is reserved
and must be escaped with the preceding '\'The message text contains a character that MarkdownV2 treats as syntax. One unescaped dot is enough — Telegram rejects the whole message. Here is every cause, most common first.
Cause 1: unescaped reserved characters
In MarkdownV2, these 18 characters are reserved and must be escaped with a backslash anywhere they appear as plain text:
_ * [ ] ( ) ~ ` > # + - = | { } . !That includes characters inside ordinary sentences: the dot at the end of this one, the hyphen in “e-mail”, the exclamation mark in “Hi!”. A minimal escape function:
JavaScript
const escapeMdV2 = (text) =>
text.replace(/[_*[\]()~`>#+\-=|{}.!]/g, "\\$&");Python
import re
def escape_md_v2(text: str) -> str:
return re.sub(r"([_*\[\]()~`>#+\-=|{}.!])", r"\\\1", text)Note these functions escape everything, including characters you meant as formatting. Escape only the plain-text parts, or use a real converter (see below).
Cause 2: parse_mode mismatch
Legacy Markdown and MarkdownV2 have different rules. Text written for one fails under the other — for example __underline__ is MarkdownV2-only, and legacy Markdown does not require escaping dots. Check which mode your call actually sends; many libraries default to legacy Markdown.
Cause 3: unclosed entities
Every *bold*, _italic_ or backtick pair must close. A stray * from an LLM response or user input opens an entity that never ends:
can't parse entities: Can't find end of the entity starting at byte offset 42This variant of the error is common in bots that forward AI-generated Markdown straight to Telegram.
Cause 4: special contexts have their own rules
- Inside
preandcodeentities, escape only`and\. - Inside link and custom-emoji URLs like
[text](https://example.com), escape only)and\.
The reliable fix: convert, don't hand-escape
Hand-escaping breaks the moment your text mixes real formatting with plain text. Two options that handle it correctly:
- telegramify-markdown (npm) — converts standard Markdown to valid MarkdownV2 in code.
- The free md2tg converter — paste Markdown, copy valid MarkdownV2. Also callable as an API from n8n or any HTTP client.
Alternatively, switch to parse_mode: "HTML" — HTML mode only requires escaping <, > and &, which is easier to get right.