Повідомлення
HTML і Markdown
Telegram Bot API поддерживает два режима разметки в поле text метода sendMessage (и в caption при отправке медиа): HTML и MarkdownV2. Режим задаётся параметром parse_mode. Если его не передать — текст уйдёт как есть, Telegram не экранирует и не парсит ничего.
Подготовка: хелпер telegramApi()
Используем функцию telegramApi(), которую мы сделали в главе Первый запрос: getMe. Она делает POST к api.telegram.org, проверяет HTTP-код, json_last_error() и поле ok в ответе. Здесь только вызываем её — реализацию не копируем.
<?php
// reminder: telegramApi() из главы first-request
// function telegramApi(string $token, string $method, array $params = []): array { ... }
$token = getenv('TELEGRAM_BOT_TOKEN') ?: exit('TELEGRAM_BOT_TOKEN not set');
$chatId = 123456789; // замените на реальный chat_id (см. главу chat-user-ids)
HTML-режим: самый предсказуемый для веб-разработчика
Поддерживаемые теги: <b>, <i>, <u>, <s>, <strike>, <del>, <code>, <pre>, <a href="...">, <tg-spoiler>. Перенос строки — обычный \n. Тег <br> не работает — он уйдёт в текст буквально.
Правило №1: всегда экранируйте пользовательский ввод через htmlspecialchars(). Иначе имя <script>alert(1)</script> сломает разметку или вернёт ошибку 400 Bad Request: can't parse entities.
<?php
$safe = static fn(string $s): string =
htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$name = "Ivan <script>alert('xss')</script>";
$comment = "Нормальный комментарий\nсо строкой";
$response = telegramApi($token, 'sendMessage', [
'chat_id' => $chatId,
'parse_mode' => 'HTML',
'text' => "<b>Новая заявка</b>\n"
. "Имя: {$safe($name)}\n"
. "Комментарий: {$safe($comment)}",
]);
// Как посмотреть ответ API в терминале:
var_dump($response);
// или для читаемости:
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
Пример успешного ответа API:
{
"ok": true,
"result": {
"message_id": 42,
"from": {
"id": 987654321,
"is_bot": true,
"first_name": "MyBot",
"username": "mybot"
},
"chat": {
"id": 123456789,
"first_name": "Ivan",
"type": "private"
},
"date": 1700000000,
"text": "<b>Новая заявка</b>\nИмя: Ivan <script>alert('xss')</script>\nКомментарий: Нормальный комментарий\nсо строкой"
}
}
Поле ok — признак успеха. В result приходит объект Message с message_id (нужен для редактирования/удаления, см. главу Редактирование и удаление) и экранированным текстом, как его увидел Telegram.
Ссылки и спойлеры
Используем функцию telegramApi() из главы Первый запрос: getMe.
<?php
$orderId = 123;
$details = "Секретная информация для клиента";
$response = telegramApi($token, 'sendMessage', [
'chat_id' => $chatId,
'parse_mode' => 'HTML',
'text' => "<a href=\"https://example.com/order/{$orderId}\">Заказ #{$orderId}</a>\n"
. "<tg-spoiler>{$safe($details)}</tg-spoiler>",
]);
if (!$response['ok']) {
// Ошибка парсинга — смотрим description
error_log('sendMessage failed: ' . ($response['description'] ?? 'unknown'));
}
Типичная ошибка невалидного HTML (забыли экранировать < в пользовательском тексте):
{
"ok": false,
"error_code": 400,
"description": "Bad Request: can't parse entities: Character '<' is reserved and must be escaped with the corresponding HTML entity"
}
В description часто пишут номер символа (byte offset) — это помогает найти проблему в длинном тексте.
MarkdownV2: мощнее, но капризнее
MarkdownV2 поддерживает цитаты (>), списки (-, 1.), inline-код (`code`), блоки кода (```php ... ```). Но он требует: любой спецсимвол _ * [ ] ( ) ~ ` > # + - = | { } . ! в тексте должен быть экранирован обратным слэшем \.
Используем функцию telegramApi() из главы Первый запрос: getMe.
<?php
$escapeV2 = static fn(string $s): string =
addcslashes($s, '_*[]()~`>#+-=|{}.!');
$name = "O'Reilly & Co.";
$sum = "1_000.50";
$orderId = 456;
$response = telegramApi($token, 'sendMessage', [
'chat_id' => $chatId,
'parse_mode' => 'MarkdownV2',
'text' => "*Заказ \\#{$orderId}*\n"
. "Клиент: {$escapeV2($name)}\n"
. "Сумма: {$escapeV2($sum)} руб.",
]);
var_dump($response);
Если забыть экранировать _ в сумме 1_000.50 — Telegram вернёт 400: can't parse entities и укажет позицию ошибки. В MarkdownV2 даже точка в конце предложения должна быть экранирована, если она не часть разметки.
Блоки кода с подсветкой синтаксиса
В HTML-режиме используйте <pre language="php">...</pre> — Telegram подсветит синтаксис. Внутри <pre> экранирование не нужно (тег защищает содержимое), но внешние данные перед вставкой в <pre> всё равно пропускайте через htmlspecialchars на случай </pre> в тексте.
Используем функцию telegramApi() из главы Первый запрос: getMe.
<?php
$code = '<?php echo "Hello"; ?>';
$response = telegramApi($token, 'sendMessage', [
'chat_id' => $chatId,
'parse_mode' => 'HTML',
'text' => "Пример кода:\n<pre language=\"php\">{$safe($code)}</pre>",
]);
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
В MarkdownV2 блоки кода оформляются тройными обратными кавычками с указанием языка: ```php\ncode\n```. Внутри экранирование не требуется.
Что выбрать: чек-лист
- HTML — если в сообщении есть пользовательские данные (имена, комментарии, адреса). Экранирование одной функцией
htmlspecialchars, меньше сюрпризов. - MarkdownV2 — если текст полностью под вашим контролем (статические шаблоны, хелпы, меню) и нужны списки/цитаты/моноширинный код в строке.
- Без parse_mode — простые уведомления одной строкой.
\nвсё равно работают как переносы.
Типичные ошибки и как их ловить
- Забыли экранировать —
400 Bad Request: can't parse entities. Смотритеdescriptionв ответе, там часто пишут номер символа (byte offset). <br>вместо\n— тег уйдёт в текст буквально. В HTML/Markdown перенос только\n.- Передали и
parse_mode, иentities— конфликт параметров, вернёт 400. - Текст длиннее 4096 символов — нужно резать на части или использовать
sendDocumentс файлом (см. главу Фото, документы, media group).
Для отладки всегда логируйте полный ответ:
Используем функцию telegramApi() из главы Первый запрос: getMe.
<?php
$response = telegramApi($token, 'sendMessage', [
'chat_id' => $chatId,
'text' => 'Test',
]);
if (!$response['ok']) {
// В description часто есть подсказка: "can't parse entities at byte offset 42"
error_log('Telegram API error: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
// Можно прислать админу в отдельный чат
$adminChatId = 999999999;
telegramApi($token, 'sendMessage', [
'chat_id' => $adminChatId,
'text' => "⚠️ Ошибка отправки: {$response['description']}",
]);
}
Дальше: Фото, документы, media group.