- Зачем нужны клавиатуры в Telegram-ботах
- Что изменилось в Telegram Bot API в 2025–2026
- Как сделать Inline Keyboard в Telegram Bot API
- Минимальный пример inline-клавиатуры
- Как работает нажатие inline-кнопки
- Пример Inline Keyboard на Python
- Reply Keyboard (ReplyKeyboardMarkup)
- Что это
- Основные параметры
- Полный пример: Reply Keyboard на Python
- Специальные типы кнопок Reply Keyboard
- Удаление Reply Keyboard
- Как показать клавиатуру одному пользователю в Telegram: канал, группа и selective=True
- Пример для python-telegram-bot v20+
- Пример для aiogram
- Что делать, если это именно канал
- Пример ограничения inline-кнопки по пользователю:
- Inline Keyboard (InlineKeyboardMarkup)
- Что это
- Полный пример: Inline Keyboard с callback_data
- Важные нюансы callback_data
- answer_callback_query: почему это обязательно
- Редактирование клавиатуры: edit_message_reply_markup
- Цветные стили кнопок: danger, success и primary
- Как это работает
- Премиум-эмодзи в тексте кнопок
- Пример: цветные кнопки на Python
- WebApp-кнопки — открытие Mini App
- 1. Reply Keyboard с Web App
- 2. Inline-кнопка с Web App
- 3. Menu Button — постоянная кнопка
- Кнопки оплаты Stars
- Как работает оплата через Stars
- Полный пример: оплата Stars
- sendMessageDraft: временный черновик ответа
- Как работает sendMessageDraft
- Пример: стриминг ответа от LLM
- Полная таблица всех типов кнопок (актуально для 2026)
- Когда какой тип использовать
- Лучшие практики
- 1. Не перегружайте клавиатуру
- 2. Всегда отвечайте на callback_query
- 3. Редактируйте, а не отправляйте заново
- 4. Цветные кнопки — для CTA, не для всех кнопок
- 5. Обрабатывайте неизвестные callback_data
- 6. Группируйте кнопки логически
- FAQ
- Чем отличается InlineKeyboard от ReplyKeyboard?
- Как сделать кнопку, которая открывает Mini App?
- Можно ли задать цвет кнопки?
- Как принять оплату через кнопку?
- Как сделать стриминг ответов как в ChatGPT?
- Сколько кнопок можно разместить в одном ряду?
Зачем нужны клавиатуры в Telegram-ботах
Telegram-бот без кнопок — это бот из 2015 года. Пользователи давно привыкли к интерактивным интерфейсам: нажал кнопку — получил результат. Никаких текстовых команд, никакого ручного ввода. Кнопки и клавиатуры — главный инструмент, который превращает бота из текстового терминала в полноценное приложение.
Что дают кнопки в практическом смысле:
- Удобство — пользователь не запоминает команды, а выбирает из предложенных вариантов.
- Скорость — одно нажатие вместо набора текста.
- Снижение ошибок — бот получает предсказуемые данные, а не произвольный текст.
- Конверсия — кнопка «Купить» или «Оставить заявку» работает лучше, чем текстовая инструкция.
В Telegram Bot API существует два принципиально разных класса клавиатур:
- Reply Keyboard (ReplyKeyboardMarkup) — кнопки, которые заменяют стандартную клавиатуру телефона. Отображаются внизу экрана.
- Inline Keyboard (InlineKeyboardMarkup) — кнопки, которые прикреплены непосредственно к сообщению. Отображаются под текстом сообщения
Что изменилось в Telegram Bot API в 2025–2026
Telegram Bot API регулярно обновляется, поэтому при работе с кнопками важно смотреть не только на старые примеры из библиотек, но и на актуальную официальную документацию. На момент обновления статьи актуальной является ветка Bot API 10.0 от 8 мая 2026 года.
- Bot API 9.0 (лето 2025) — улучшенная поддержка Mini Apps, новые параметры для KeyboardButtonRequestUsers.
- Bot API 9.3 (январь 2026) — появление
sendMessageDraftдля стриминга ответов бота, аналогично тому, как отвечает ChatGPT. - Bot API 9.4 (февраль 2026) — цветные кнопки: параметр
button_colorв InlineKeyboardButton, поддержка HEX-цветов и премиум-эмодзи в тексте кнопок. - Bot API 9.5 (март 2026) —
sendMessageDraftдоступен всем ботам без ограничений, расширены лимиты на количество кнопок. - Bot API 10.0 добавил guest mode, новые возможности для опросов, live photos и ряд изменений для chat management и business bots
Как сделать Inline Keyboard в Telegram Bot API

Если вам нужно быстро добавить кнопки под сообщением в Telegram-боте, используйте Inline Keyboard. Такая клавиатура прикрепляется к конкретному сообщению, а при нажатии кнопки бот получает не обычный текст, а специальное событие — callback_query
Inline Keyboard чаще всего используют для выбора действий, пагинации, подтверждения операций, открытия ссылок, запуска Mini App и обработки нажатий через callback_data
Минимальный пример inline-клавиатуры

{
"chat_id": 123456789,
"text": "Выберите действие:",
"reply_markup": {
"inline_keyboard": [
[
{
"text": "📋 Открыть каталог",
"callback_data": "open_catalog"
}
],
[
{
"text": "🌐 Перейти на сайт",
"url": "https://aglamov.biz"
}
]
]
}
}
Главное отличие: кнопка с callback_data отправляет боту callback_query, а кнопка с url просто открывает ссылку. Если вы ждёте обработку нажатия внутри бота, используйте callback_data
- Кнопка с callback_data отправляет боту callback_query
- Кнопка с url открывает ссылку и не отправляет callback боту
- callback_data должен быть коротким — до 64 байт
- После нажатия inline-кнопки бот должен ответить на callback, иначе у пользователя будет крутиться индикатор загрузки
Как работает нажатие inline-кнопки
Пользователь нажимает кнопку
↓
Telegram отправляет боту callback_query
↓
Бот вызывает answerCallbackQuery
↓
Бот редактирует сообщение или отправляет новый ответ
Пример Inline Keyboard на Python
Ниже пример для библиотеки python-telegram-bot. Бот показывает две inline-кнопки с callback_data и одну кнопку-ссылку
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
[
InlineKeyboardButton("📋 Каталог", callback_data="catalog"),
InlineKeyboardButton("🛒 Корзина", callback_data="cart"),
],
[
InlineKeyboardButton("🌐 Сайт", url="https://example.com")
],
]
await update.message.reply_text(
"Выберите действие:",
reply_markup=InlineKeyboardMarkup(keyboard)
)
async def handle_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
# Обязательно отвечаем на callback, чтобы убрать индикатор загрузки
await query.answer()
if query.data == "catalog":
await query.edit_message_text("Вы открыли каталог товаров.")
elif query.data == "cart":
await query.edit_message_text("Ваша корзина пока пуста.")
def main() -> None:
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CallbackQueryHandler(handle_button))
app.run_polling()
if __name__ == "__main__":
main()
если бот не вызывает await query.answer(), пользователь может видеть бесконечный индикатор загрузки после нажатия inline-кнопки. Поэтому обработку callback_query почти всегда начинают с query.answer()
Если вам нужно постоянное меню внизу экрана, используйте ReplyKeyboardMarkup. Если нужны кнопки именно под сообщением, обработка нажатий, пагинация, подтверждение выбора, ссылки, Mini App или оплата, используйте InlineKeyboardMarkup
Ниже подробно разберём все варианты кнопок в Telegram Bot API: Reply Keyboard, Inline Keyboard, Web App-кнопки, оплату, обработку callback_query, частые ошибки и лучшие практики
Reply Keyboard (ReplyKeyboardMarkup)
Что это
Reply Keyboard — это набор кнопок, который отображается вместо стандартной клавиатуры телефона. Когда пользователь нажимает на такую кнопку, бот получает обычное текстовое сообщение с текстом кнопки. По сути, это быстрый ввод текста одним нажатием.
Reply Keyboard удобен для:
- Главного меню бота
- Навигации по разделам
- Выбора из фиксированного списка вариантов
Основные параметры
| Параметр | Тип | Описание |
|---|---|---|
keyboard | list[list[KeyboardButton]] | Двумерный массив кнопок (ряды и столбцы) |
resize_keyboard | bool | Подгоняет размер клавиатуры под количество кнопок. По умолчанию False — клавиатура занимает столько же места, сколько стандартная |
one_time_keyboard | bool | Скрывает клавиатуру после нажатия кнопки |
input_field_placeholder | str | Текст-подсказка в поле ввода (до 64 символов) |
selective | bool | Показывает клавиатуру только определённым пользователям в группе |
is_persistent | bool | Клавиатура остаётся видимой даже после перезапуска бота |
Полный пример: Reply Keyboard на Python
from telegram import Update, ReplyKeyboardMarkup, KeyboardButton
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Отправляет приветствие и показывает Reply Keyboard."""
keyboard = [
[KeyboardButton("📋 Каталог"), KeyboardButton("🛒 Корзина")],
[KeyboardButton("📞 Поддержка"), KeyboardButton("ℹ️ О нас")],
[KeyboardButton("📍 Отправить геолокацию", request_location=True)],
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False,
input_field_placeholder="Выберите раздел..."
)
await update.message.reply_text(
"Добро пожаловать! Выберите раздел:",
reply_markup=reply_markup
)
async def handle_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обрабатывает нажатия кнопок Reply Keyboard."""
text = update.message.text
if text == "📋 Каталог":
await update.message.reply_text("Вот наш каталог товаров...")
elif text == "🛒 Корзина":
await update.message.reply_text("Ваша корзина пуста.")
elif text == "📞 Поддержка":
await update.message.reply_text("Напишите ваш вопрос, мы ответим в течение часа.")
elif text == "ℹ️ О нас":
await update.message.reply_text("Мы — команда разработчиков ботов.")
async def handle_location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обрабатывает полученную геолокацию."""
location = update.message.location
await update.message.reply_text(
f"Ваши координаты: {location.latitude}, {location.longitude}"
)
def main() -> None:
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("start", start))
app.add_handler(MessageHandler(filters.LOCATION, handle_location))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_menu))
app.run_polling()
if __name__ == "__main__":
main()
Специальные типы кнопок Reply Keyboard
Помимо обычного текста, кнопки Reply Keyboard могут запрашивать у пользователя специальные данные:
from telegram import KeyboardButton, KeyboardButtonRequestChat, KeyboardButtonRequestUsers
# Запрос контакта (номер телефона)
btn_contact = KeyboardButton("📱 Отправить контакт", request_contact=True)
# Запрос геолокации
btn_location = KeyboardButton("📍 Отправить местоположение", request_location=True)
# Запрос создания опроса
btn_poll = KeyboardButton("📊 Создать опрос", request_poll=KeyboardButtonPollType(type="quiz"))
# Запрос выбора чата (Bot API 6.4+)
btn_chat = KeyboardButton(
"Выбрать группу",
request_chat=KeyboardButtonRequestChat(
request_id=1,
chat_is_channel=False,
bot_is_member=True # бот должен быть участником выбранного чата
)
)
# Запрос выбора пользователей (Bot API 6.5+)
btn_users = KeyboardButton(
"Выбрать пользователей",
request_users=KeyboardButtonRequestUsers(
request_id=2,
user_is_bot=False,
max_quantity=5 # можно выбрать до 5 пользователей
)
)
KeyboardButtonRequestChat и KeyboardButtonRequestUsers особенно полезны, когда бот должен работать с конкретной группой или пользователем — например, для настройки рассылки или назначения администратора.
Удаление Reply Keyboard
Чтобы убрать Reply Keyboard, используйте ReplyKeyboardRemove:
from telegram import ReplyKeyboardRemove
await update.message.reply_text(
"Клавиатура скрыта.",
reply_markup=ReplyKeyboardRemove()
)
Как показать клавиатуру одному пользователю в Telegram: канал, группа и selective=True
В Telegram Bot API важно различать канал и группу / супергруппу. Если речь именно о Telegram-канале, показать ReplyKeyboardMarkup только одному подписчику нельзя: Telegram официально указывает, что ReplyKeyboardMarkup не поддерживается в каналах. Такая клавиатура предназначена для личных чатов, групп и супергрупп, но не для канальных постов.
Если же под «каналом» имеется в виду общий чат / группа, тогда персональная клавиатура возможна через параметр selective=True. Но selective — это не список user_id. Это булевый флаг, а конкретный пользователь определяется Telegram автоматически: либо пользователь упомянут в тексте сообщения, либо бот отвечает реплаем на сообщение этого пользователя. Поэтому надёжный вариант — отправлять клавиатуру именно как replyна команду пользователя
Пример для python-telegram-bot v20+
from telegram import Update, ReplyKeyboardMarkup
from telegram.ext import Application, CommandHandler, ContextTypes
async def game(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
["Ответ 1"],
["Ответ 2"],
["Отмена"],
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=True,
selective=True,
)
await update.message.reply_text(
"Выберите вариант ответа:",
reply_markup=reply_markup,
)
def main() -> None:
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("game", game))
app.run_polling()
if __name__ == "__main__":
main()
Ключевой момент: использовать именно reply_text() / message.reply(), а не просто send_message(chat_id=group_id, ...). Если отправить обычное сообщение в группу без привязки к сообщению пользователя, Telegram не поймёт, кому именно показывать selective-клавиатуру
Пример для aiogram
from aiogram import Router, types
from aiogram.filters import Command
router = Router()
@router.message(Command("game"))
async def game(message: types.Message):
keyboard = types.ReplyKeyboardMarkup(
keyboard=[
[types.KeyboardButton(text="Ответ 1")],
[types.KeyboardButton(text="Ответ 2")],
[types.KeyboardButton(text="Отмена")],
],
resize_keyboard=True,
one_time_keyboard=True,
selective=True,
)
await message.reply(
"Выберите вариант ответа:",
reply_markup=keyboard,
)
Здесь message.reply() важен по той же причине: бот отвечает на сообщение конкретного пользователя, и Telegram показывает клавиатуру только ему
Что делать, если это именно канал
Для настоящего Telegram-канала есть только обходные варианты:
- Отправить клавиатуру в личный чат пользователю через
send_message(user_id, reply_markup=...). Но пользователь должен заранее открыть бота и нажать/start, иначе бот не сможет сам начать с ним диалог. - Использовать Inline Keyboard в канальном посте, но понимать ограничение: inline-кнопки будут видны всем. Можно ограничить обработку на стороне бота — например, принимать нажатие только от нужного
user_id, а остальным показывать уведомление «Эта кнопка не для вас». Inline Keyboard прикрепляется к сообщению и работает черезCallbackQuery, а не через замену пользовательской клавиатуры
Пример ограничения inline-кнопки по пользователю:
ALLOWED_USER_ID = 123456789
async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
if query.from_user.id != ALLOWED_USER_ID:
await query.answer("Эта кнопка не для вас.", show_alert=True)
return
await query.answer("Доступ разрешён.")
await query.edit_message_text("Вы нажали персональную кнопку.")
Inline Keyboard (InlineKeyboardMarkup)
Что это
Inline Keyboard — кнопки, которые прикреплены непосредственно к конкретному сообщению бота. Они не заменяют клавиатуру, а отображаются под текстом сообщения. При нажатии на inline-кнопку пользователь не отправляет текстовое сообщение — вместо этого бот получает специальный объект CallbackQuery.
Inline-кнопки поддерживают несколько режимов работы:
- callback_data — бот получает callback с произвольными данными (до 64 байт).
- url — кнопка открывает ссылку в браузере.
- switch_inline_query — переключает пользователя в inline-режим бота.
- switch_inline_query_current_chat — аналогично, но в текущем чате.
- login_url — авторизация через Telegram Login Widget.
- web_app — открывает Mini App (подробнее в разделе ниже).
- pay — кнопка оплаты (подробнее в разделе про Stars).
Полный пример: Inline Keyboard с callback_data
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
async def show_products(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Показывает список товаров с inline-кнопками."""
keyboard = [
[
InlineKeyboardButton("🔵 Тариф Basic — $9/мес", callback_data="plan_basic"),
InlineKeyboardButton("🟢 Тариф Pro — $29/мес", callback_data="plan_pro"),
],
[
InlineKeyboardButton("🟡 Тариф Enterprise — $99/мес", callback_data="plan_enterprise"),
],
[
InlineKeyboardButton("📖 Подробнее о тарифах", url="https://example.com/pricing"),
],
[
InlineKeyboardButton(
"Попробовать бота в другом чате",
switch_inline_query="тарифы"
),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"Выберите тарифный план:",
reply_markup=reply_markup
)
async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обрабатывает нажатие inline-кнопки."""
query = update.callback_query
# ОБЯЗАТЕЛЬНО: подтверждаем получение callback
await query.answer()
plan_names = {
"plan_basic": "Basic ($9/мес)",
"plan_pro": "Pro ($29/мес)",
"plan_enterprise": "Enterprise ($99/мес)",
}
plan = plan_names.get(query.data, "Неизвестный")
await query.edit_message_text(
f"Вы выбрали тариф: {plan}\n\nДля подтверждения нажмите кнопку ниже.",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("✅ Подтвердить", callback_data=f"confirm_{query.data}")],
[InlineKeyboardButton("⬅️ Назад", callback_data="back_to_plans")],
])
)
async def handle_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обрабатывает подтверждение выбора."""
query = update.callback_query
if query.data == "back_to_plans":
# Возвращаем исходную клавиатуру
await query.answer()
keyboard = [
[
InlineKeyboardButton("🔵 Basic — $9/мес", callback_data="plan_basic"),
InlineKeyboardButton("🟢 Pro — $29/мес", callback_data="plan_pro"),
],
[InlineKeyboardButton("🟡 Enterprise — $99/мес", callback_data="plan_enterprise")],
]
await query.edit_message_text(
"Выберите тарифный план:",
reply_markup=InlineKeyboardMarkup(keyboard)
)
elif query.data.startswith("confirm_"):
await query.answer("Тариф активирован! 🎉", show_alert=True)
await query.edit_message_text("Тариф успешно активирован. Спасибо!")
def main() -> None:
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("plans", show_products))
app.add_handler(CallbackQueryHandler(handle_confirm, pattern="^(confirm_|back_)"))
app.add_handler(CallbackQueryHandler(handle_callback, pattern="^plan_"))
app.run_polling()
if __name__ == "__main__":
main()
Важные нюансы callback_data
Параметр callback_data ограничен 64 байтами. Не пытайтесь передать в нём JSON-объект — используйте короткие идентификаторы:
# Плохо:
callback_data='{"action":"buy","product_id":12345,"quantity":2}'
# Хорошо:
callback_data="buy_12345_2"
Для хранения состояния используйте context.user_data или внешнее хранилище (Redis, базу данных).
answer_callback_query: почему это обязательно
После каждого нажатия inline-кнопки Telegram показывает индикатор загрузки. Если бот не вызовет answer_callback_query в течение 30 секунд, пользователь увидит ошибку. Поэтому всегда вызывайте await query.answer(), даже если вам не нужно показывать уведомление.
# Тихое подтверждение (без уведомления)
await query.answer()
# Всплывающее уведомление (toast)
await query.answer("Товар добавлен в корзину")
# Модальное окно с кнопкой OK
await query.answer("Оплата прошла успешно!", show_alert=True)
Редактирование клавиатуры: edit_message_reply_markup
Вместо отправки нового сообщения при каждом нажатии кнопки используйте редактирование существующего:
async def toggle_setting(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer()
# Переключаем настройку
current = context.user_data.get("notifications", True)
context.user_data["notifications"] = not current
new_status = "✅ Вкл" if not current else "❌ Выкл"
new_keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton(
f"Уведомления: {new_status}",
callback_data="toggle_notifications"
)],
])
# Меняем только клавиатуру, не трогая текст сообщения
await query.edit_message_reply_markup(reply_markup=new_keyboard)
Цветные стили кнопок: danger, success и primary
Начиная с Bot API 9.4, у кнопок KeyboardButton и InlineKeyboardButton появилось поле style. Оно позволяет визуально выделять кнопку одним из трёх стандартных стилей: danger, success или primary
Значение style | Как выглядит | Когда использовать |
| danger | Красный акцент | Удаление, отмена, опасные действия |
| success | Зелёный акцент | Подтверждение, успешное действие, покупка |
| primary | Синий акцент | Основное действие, переход дальше, запуск сценария |
Важно: style — это не произвольный HEX-цвет. Нельзя указать любой цвет кнопки через background_color или text_color. Telegram даёт только готовые стили, а точный внешний вид зависит от клиента Telegram
Как это работает
Параметр button_color принимает объект InlineKeyboardButtonColor с тремя полями:
| Поле | Тип | Описание |
|---|---|---|
background_color | int | Цвет фона в формате RGB (24-bit integer) |
text_color | int | Цвет текста в формате RGB (24-bit integer) |
Цвета задаются как целое число, соответствующее HEX-коду. Например, 0xFF0000 — красный, 0x00FF00 — зелёный, 0xFFFFFF — белый.
Премиум-эмодзи в тексте кнопок
Начиная с Bot API 9.4, в текст кнопок можно добавлять кастомные премиум-эмодзи через text_entities. Это позволяет использовать брендовые иконки прямо в кнопках.
Пример: цветные кнопки на Python
{
"chat_id": 123456789,
"text": "Подтвердить заказ №1284?",
"reply_markup": {
"inline_keyboard": [
[
{
"text": "✅ Подтвердить",
"callback_data": "confirm_order_1284",
"style": "success"
},
{
"text": "❌ Отмена",
"callback_data": "cancel_order_1284",
"style": "danger"
}
],
[
{
"text": "Подробнее",
"callback_data": "order_details_1284",
"style": "primary"
}
]
]
}
}
Цветные кнопки отображаются во всех актуальных клиентах Telegram (Android, iOS, Desktop). На старых версиях клиентов кнопка будет выглядеть стандартно — цвет просто игнорируется.
Если в вашей библиотеке для Python, Node.js или PHP поле style пока не поддерживается, можно отправить запрос напрямую в Bot API как JSON или обновить библиотеку до версии, которая уже поддерживает новые поля Telegram Bot API
WebApp-кнопки — открытие Mini App
Mini Apps (ранее Web Apps) — это полноценные веб-приложения, встроенные в интерфейс Telegram. Кнопки — основной способ открыть Mini App из бота. Существует три подхода, каждый для своего сценария.
Подробнее о создании Mini Apps читайте в нашем руководстве: Как создать своё приложение в Telegram и добавить в Telegram Apps Center.
1. Reply Keyboard с Web App
Кнопка отображается вместо клавиатуры. При нажатии открывается Mini App в модальном окне:
from telegram import Update, ReplyKeyboardMarkup, KeyboardButton, WebAppInfo
from telegram.ext import Application, CommandHandler, ContextTypes
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
[
KeyboardButton(
"📱 Открыть приложение",
web_app=WebAppInfo(url="https://your-app.com")
)
],
[KeyboardButton("📞 Поддержка")],
]
await update.message.reply_text(
"Нажмите кнопку, чтобы открыть приложение:",
reply_markup=ReplyKeyboardMarkup(keyboard, resize_keyboard=True)
)
def main() -> None:
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("start", start))
app.run_polling()
if __name__ == "__main__":
main()
2. Inline-кнопка с Web App
Кнопка прикреплена к сообщению. Mini App открывается при нажатии:
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo
from telegram.ext import Application, CommandHandler, ContextTypes
async def shop(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
[
InlineKeyboardButton(
"🛒 Открыть магазин",
web_app=WebAppInfo(url="https://shop.example.com")
)
],
[
InlineKeyboardButton(
"📋 Каталог товаров",
web_app=WebAppInfo(url="https://shop.example.com/catalog")
)
],
]
await update.message.reply_text(
"Добро пожаловать в наш магазин!",
reply_markup=InlineKeyboardMarkup(keyboard)
)
def main() -> None:
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("shop", shop))
app.run_polling()
if __name__ == "__main__":
main()
3. Menu Button — постоянная кнопка
Menu Button отображается слева от поля ввода текста и доступен пользователю всегда, без отправки каких-либо команд. Идеально подходит для основного интерфейса бота:
from telegram import Update, MenuButtonWebApp, WebAppInfo
from telegram.ext import Application, CommandHandler, ContextTypes
async def setup_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Устанавливает постоянную кнопку меню с Mini App."""
await context.bot.set_chat_menu_button(
chat_id=update.effective_chat.id,
menu_button=MenuButtonWebApp(
text="Каталог",
web_app=WebAppInfo(url="https://catalog.example.com")
)
)
await update.message.reply_text(
"Кнопка меню установлена! Нажмите на неё слева от поля ввода."
)
async def remove_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Сбрасывает кнопку меню на стандартную."""
from telegram import MenuButtonDefault
await context.bot.set_chat_menu_button(
chat_id=update.effective_chat.id,
menu_button=MenuButtonDefault()
)
await update.message.reply_text("Кнопка меню сброшена.")
def main() -> None:
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("setup_menu", setup_menu))
app.add_handler(CommandHandler("remove_menu", remove_menu))
app.run_polling()
if __name__ == "__main__":
main()
Пошаговую интеграцию Mini App в бота мы разобрали в отдельной статье: Интеграция Mini Apps в боты Telegram: пошаговое руководство.
Кнопки оплаты Stars
Telegram Stars — внутренняя валюта Telegram для оплаты цифровых товаров и услуг. Боты могут принимать оплату в Stars через встроенный платёжный интерфейс, без внешних платёжных систем.
Как работает оплата через Stars
- Бот отправляет invoice (счёт) с
currency="XTR"(код валюты Stars). - Пользователь видит сообщение с кнопкой оплаты.
- Telegram отправляет боту
pre_checkout_query— бот подтверждает или отклоняет платёж. - После успешной оплаты бот получает
successful_payment.
Полный пример: оплата Stars
from telegram import Update, LabeledPrice, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application,
CommandHandler,
PreCheckoutQueryHandler,
MessageHandler,
filters,
ContextTypes,
)
async def sell_item(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Отправляет счёт на оплату в Stars."""
await context.bot.send_invoice(
chat_id=update.effective_chat.id,
title="Премиум-подписка на 1 месяц",
description="Доступ ко всем функциям бота: аналитика, отчёты, приоритетная поддержка.",
payload="premium_1month", # внутренний идентификатор товара
currency="XTR", # Stars
prices=[LabeledPrice("Премиум-подписка", 150)], # 150 Stars
# Для Stars не нужен provider_token
)
async def precheckout_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обрабатывает pre-checkout запрос. Нужно ответить в течение 10 секунд."""
query = update.pre_checkout_query
# Проверяем, что payload корректный
if query.invoice_payload == "premium_1month":
await query.answer(ok=True)
else:
await query.answer(ok=False, error_message="Ошибка: неизвестный товар.")
async def successful_payment_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Вызывается после успешной оплаты."""
payment = update.message.successful_payment
await update.message.reply_text(
f"Оплата прошла успешно!\n"
f"Товар: {payment.invoice_payload}\n"
f"Сумма: {payment.total_amount} Stars\n"
f"ID транзакции: {payment.telegram_payment_charge_id}\n\n"
f"Премиум-подписка активирована. Спасибо!"
)
# Здесь активируем подписку в базе данных
# await activate_premium(update.effective_user.id, months=1)
async def refund_stars(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Возврат Stars пользователю (для администратора)."""
# charge_id — ID транзакции из successful_payment
charge_id = context.args[0] if context.args else None
if not charge_id:
await update.message.reply_text("Использование: /refund <charge_id>")
return
try:
await context.bot.refund_star_payment(
user_id=update.effective_user.id,
telegram_payment_charge_id=charge_id
)
await update.message.reply_text("Возврат выполнен.")
except Exception as e:
await update.message.reply_text(f"Ошибка возврата: {e}")
def main() -> None:
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("buy", sell_item))
app.add_handler(PreCheckoutQueryHandler(precheckout_handler))
app.add_handler(MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment_handler))
app.add_handler(CommandHandler("refund", refund_stars))
app.run_polling()
if __name__ == "__main__":
main()
Кнопка оплаты всегда отображается первой в клавиатуре: InlineKeyboardButton(pay=True) автоматически помещается в первый ряд, независимо от порядка в коде.
sendMessageDraft: временный черновик ответа
sendMessageDraft — метод Telegram Bot API для показа пользователю промежуточного ответа, пока бот генерирует сообщение. Это полезно для AI-ботов, ассистентов, ботов поддержки и любых сценариев, где ответ может формироваться несколько секунд
Важно понимать ограничение: sendMessageDraft не создаёт постоянное сообщение в чате. Это временный черновик. Когда ответ готов, бот должен отправить финальный текст обычным методом sendMessage
Как работает sendMessageDraft
Бот начинает генерировать ответ
↓
Отправляет первый черновик через sendMessageDraft
↓
Обновляет черновик с тем же draft_id
↓
Когда ответ готов, отправляет финальное сообщение через sendMessage
Метод появился в Bot API 9.3 (январь 2026) в ограниченном тестовом режиме. С Bot API 9.5 (март 2026) sendMessageDraft доступен всем ботам без ограничений.
Пример: стриминг ответа от LLM
import requests
import time
TOKEN = "YOUR_BOT_TOKEN"
CHAT_ID = 123456789
DRAFT_ID = 1001
API_URL = f"https://api.telegram.org/bot{TOKEN}"
def send_message_draft(text: str) -> None:
requests.post(
f"{API_URL}/sendMessageDraft",
json={
"chat_id": CHAT_ID,
"draft_id": DRAFT_ID,
"text": text,
},
timeout=10,
)
def send_final_message(text: str) -> None:
requests.post(
f"{API_URL}/sendMessage",
json={
"chat_id": CHAT_ID,
"text": text,
},
timeout=10,
)
parts = [
"Готовлю ответ...",
"Готовлю ответ...\n\nПроверяю данные...",
"Готовлю ответ...\n\nПроверяю данные...\n\nФормирую итоговый текст...",
]
for part in parts:
send_message_draft(part)
time.sleep(1)
final_text = (
"Готово. Inline Keyboard используется для кнопок под сообщением. "
"Если кнопка содержит callback_data, бот получает callback_query. "
"Если кнопка содержит url, пользователь просто открывает ссылку."
)
send_final_message(final_text)
Не путайте sendMessageDraft с обычным сообщением. Черновик нужен только для временного отображения процесса генерации. Финальный результат всё равно нужно отправить через sendMessage
Также у sendMessageDraft есть обязательный параметр draft_id. Он должен быть ненулевым. Если обновлять черновики с одним и тем же draft_id, изменения будут отображаться как продолжение одного и того же временного ответа
Стриминг особенно полезен для AI-ботов. Если вы разрабатываете бота с интеграцией нейросети, рекомендуем также ознакомиться с нашим руководством по созданию интерактивных приложений: Как создать тапалку для Telegram: пошаговое руководство.
Полная таблица всех типов кнопок (актуально для 2026)
| Тип | Класс | Где отображается | Для чего |
|---|---|---|---|
| Reply Keyboard | ReplyKeyboardMarkup | Вместо клавиатуры | Меню, навигация |
| Inline Keyboard | InlineKeyboardMarkup | Под сообщением | Действия, callback |
| Web App Button | KeyboardButton(web_app=...) | Вместо клавиатуры | Открытие Mini App |
| Inline Web App | InlineKeyboardButton(web_app=...) | Под сообщением | Открытие Mini App |
| Menu Button | MenuButtonWebApp | Рядом с полем ввода | Постоянный доступ к Mini App |
| Pay Button | InlineKeyboardButton(pay=True) | Под сообщением | Оплата Stars |
| Login URL | InlineKeyboardButton(login_url=...) | Под сообщением | Авторизация через Telegram |
| Switch Inline | InlineKeyboardButton(switch_inline_query=...) | Под сообщением | Переход в inline-режим |
| Цветная кнопка (NEW) | InlineKeyboardButton(button_color=...) | Под сообщением | Визуальный акцент, CTA |
Когда какой тип использовать
- Reply Keyboard — для постоянного меню, к которому пользователь обращается регулярно. Пример: главное меню бота, выбор языка.
- Inline Keyboard — для действий, привязанных к конкретному сообщению. Пример: лайк/дизлайк, выбор товара, пагинация.
- Web App Button — когда нужен сложный интерфейс, который невозможно реализовать кнопками: формы, карты, дашборды.
- Menu Button — когда Mini App является основным интерфейсом бота.
- Pay Button — для продажи цифровых товаров за Stars.
- Цветная кнопка — для выделения ключевого действия среди нескольких кнопок.
Лучшие практики
1. Не перегружайте клавиатуру
Максимум 3–4 кнопки в одном ряду. На мобильных устройствах узкие кнопки невозможно нажать точно. Если кнопок много — разбивайте на несколько рядов или используйте пагинацию:
# Плохо: 6 кнопок в одном ряду
keyboard = [[btn1, btn2, btn3, btn4, btn5, btn6]]
# Хорошо: по 3 кнопки в ряду
keyboard = [
[btn1, btn2, btn3],
[btn4, btn5, btn6],
]
2. Всегда отвечайте на callback_query
Каждое нажатие inline-кнопки должно получить ответ через answer_callback_query. Без этого пользователь видит бесконечный индикатор загрузки. Даже если обработка занимает время, сначала подтвердите получение:
async def handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer("Загрузка...") # Сразу отвечаем
# ... долгая операция
3. Редактируйте, а не отправляйте заново
Используйте edit_message_text и edit_message_reply_markup вместо отправки нового сообщения. Это сохраняет чат чистым и не создаёт спам из сообщений бота.
4. Цветные кнопки — для CTA, не для всех кнопок
Если все кнопки будут цветными, ни одна из них не будет выделяться. Используйте button_color только для одного ключевого действия: «Купить», «Подтвердить», «Начать». Остальные кнопки оставляйте стандартными.
5. Обрабатывайте неизвестные callback_data
Если пользователь нажмёт кнопку из старого сообщения, callback_data может не совпадать ни с одним из текущих обработчиков. Добавьте fallback-обработчик:
async def unknown_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer("Эта кнопка устарела. Используйте /start для начала.", show_alert=True)
# Добавляйте последним — он сработает, если не сработал ни один другой
app.add_handler(CallbackQueryHandler(unknown_callback))
6. Группируйте кнопки логически
Связанные действия размещайте в одном ряду, разные категории — в разных рядах. Деструктивные действия (удаление, отмена) визуально отделяйте от основных.
FAQ
Чем отличается InlineKeyboard от ReplyKeyboard?
ReplyKeyboard заменяет стандартную клавиатуру и отображается внизу экрана. При нажатии отправляется текстовое сообщение с текстом кнопки. InlineKeyboard прикреплена к конкретному сообщению и отображается под ним. При нажатии бот получает CallbackQuery — никакое сообщение в чат не отправляется.
Используйте Reply Keyboard для постоянного меню, Inline Keyboard — для действий в контексте конкретного сообщения.
Как сделать кнопку, которая открывает Mini App?
Три способа:
- Reply Keyboard:
KeyboardButton(text="Открыть", web_app=WebAppInfo(url="https://...")) - Inline-кнопка:
InlineKeyboardButton(text="Открыть", web_app=WebAppInfo(url="https://...")) - Menu Button:
MenuButtonWebApp(text="Меню", web_app=WebAppInfo(url="https://..."))
URL приложения должен быть HTTPS. Подробности — в нашем руководстве по интеграции Mini Apps в боты Telegram.
Можно ли задать цвет кнопки?
Да, с Bot API 9.4 (февраль 2026). Используйте параметр button_color в InlineKeyboardButton:
InlineKeyboardButton(
"Купить",
callback_data="buy",
button_color={"background_color": 0x43A047, "text_color": 0xFFFFFF}
)
Цветные кнопки работают только для Inline Keyboard. Reply Keyboard не поддерживает кастомные цвета.
Как принять оплату через кнопку?
Используйте метод send_invoice с currency="XTR" для оплаты в Telegram Stars. Для Stars не нужен provider_token — оплата обрабатывается внутри Telegram. Бот должен обрабатывать два события: pre_checkout_query (подтверждение перед оплатой) и successful_payment (после оплаты). Полный пример кода — в разделе «Кнопки оплаты Stars» выше.
Как сделать стриминг ответов как в ChatGPT?
Используйте метод sendMessageDraft (Bot API 9.3+, доступен всем с 9.5). Бот отправляет текст порциями, и пользователь видит постепенное появление ответа. После завершения генерации вызовите finalizeMessage, чтобы черновик стал обычным сообщением. Подробный пример — в разделе «sendMessageDraft» выше.
Сколько кнопок можно разместить в одном ряду?
Технический лимит Telegram — 8 кнопок в одном ряду для Inline Keyboard и 12 кнопок для Reply Keyboard. Однако на практике рекомендуется использовать не более 3–4 кнопок в ряду, чтобы текст на них был читаемым на мобильных устройствах. Максимальное количество рядов — 100 для inline-клавиатуры.



