Кнопки в Telegram-боте свяжите с действием: выберите reply, callback или URL, получите нужное событие и проверьте ответ. В примере вручную пройдём выбор между планом и вопросами к встрече.

Бот спрашивает, что подготовить к встрече, и предлагает «План» или «Вопросы». После нажатия хочется сразу получить нужную заготовку. Для этого за кнопкой должны стоять получение выбора, проверка и понятный ответ. На таком вопросе разберём, как связаны эти действия.

В конце упражнения в чате останется выбранная заготовка. Кнопки проверим вручную на отдельном собственном учебном боте; постоянно отвечать за вас этот опыт пока не будет. Для запросов понадобится терминал — окно, куда вставляют команды, — с Bash и curl. Если эти названия пока ни о чём не говорят, начните вместе с помощником.

Откройте DeepSeek в соседней вкладке и вставьте запрос ниже в новый чат. Подойдёт и привычный ChatGPT или Claude. Следующим сообщением скопируйте текст этой статьи.

Помоги мне по статье добавить две кнопки учебному Telegram-боту:
«План» и «Вопросы», чтобы после выбора получить заготовку к встрече.
Сначала спроси, с какого устройства я работаю и есть ли отдельный бот.
Затем веди по одному действию: что открыть, куда вставить команду
и какой результат увидеть. Жди моего ответа после каждого шага.
Если нужен терминал, объясни, как открыть его на моём устройстве
и проверить наличие Bash и curl. Токен я ввожу только у себя;
не проси его прислать. Экран и нажатия я буду описывать сам.
Если пришлю ошибку без секретов, разбери её до следующего действия.
Начни с вопроса об устройстве, без списка команд на весь процесс.

ИИ объясняет действия по вашему описанию, кнопки нажимаете вы. В ответах Telegram перед отправкой помощнику убирайте токен, имена и номера личных чатов. Сначала разберитесь с одним шагом; все длинные блоки сразу выполнять не нужно.

Кнопки в Telegram-боте: куда должен попасть ответ

У кнопки может быть разная работа. Обычная reply-кнопка отправляет в чат текст; callback-кнопка передаёт боту отдельное событие; URL-кнопка открывает ссылку. Эти различия показаны в Telegram Bot Features и официальном учебнике.

Что нужно сделать Тип кнопки Что проверять после нажатия
Предложить человеку готовую реплику «План» Reply, обычная текстовая кнопка В обновлении есть message.text со словом «План»
Получить выбор под конкретным вопросом Inline с callback_data Пришёл callback_query, сохранились отправитель и ссылка на сообщение
Открыть страницу справки Inline с url Открылась нужная страница; callback от такой кнопки не ожидается

Для выбора под нашим вопросом возьмём callback. Reply сначала проверим отдельно, чтобы увидеть разницу. Все тексты и заготовки ниже составлены для учебного примера.

При выборе «План» будем выдавать такую основу:

План встречи. Цель встречи; решение, которое нужно принять; следующий шаг и ответственный.

При выборе «Вопросы» пригодится другой набор:

Вопросы к встрече. Что мешает продвинуть задачу? Какие данные уже есть? Какое решение нужно принять?

Подготовьте учебного бота и найдите личный чат

Для регистрации бота есть инструкция по BotFather. Здесь нужен его токен и бот, к которому ещё не подключены конструктор, webhook или работающий получатель getUpdates. Для эксперимента с кнопками используйте отдельного учебного бота: чужой работающий сценарий тогда останется на месте.

Откройте Bash и введите токен скрыто. В том же окне будем выполнять все запросы.

read -r -s -p 'Токен учебного бота: ' BOT_TOKEN
printf '\n'
printf 'url = "https://api.telegram.org/bot%s/getWebhookInfo"\n' "$BOT_TOKEN" |
  curl --silent --show-error --max-time 20 --config -

Продолжайте при ok:true и пустом result.url. Метод getWebhookInfo показывает, задан ли webhook. При заполненном URL выберите другого учебного бота. Пустой URL ещё не исключает другую программу с getUpdates, поэтому её отсутствие тоже проверьте по своим подключениям.

Теперь в Telegram откройте личный чат с этим ботом и отправьте /start. Получите событие:

OFFSET=0
printf 'url = "https://api.telegram.org/bot%s/getUpdates"\n' "$BOT_TOKEN" |
  curl --silent --show-error --max-time 20 --config - --request POST \
    --data-urlencode "offset=$OFFSET" \
    --data-urlencode 'timeout=0' \
    --data-urlencode 'allowed_updates=["message","callback_query"]' \
    --output updates.json
cat updates.json

В массиве result найдите своё сообщение /start. Из его message.chat.id возьмите адрес чата, из message.from.id получите идентификатор отправителя. Введите реальные числа из ответа:

read -r -p 'message.chat.id: ' CHAT_ID
read -r -p 'message.from.id: ' OWNER_ID

Для следующих чтений используйте тот же блок getUpdates, начиная со строки printf; строку OFFSET=0 больше не повторяйте. После обработки всех полученных событий задавайте следующий offset:

read -r -p 'Наибольший обработанный update_id: ' UPDATE_ID
OFFSET=$((UPDATE_ID + 1))

Пока получен только собственный /start, запишите его update_id. Если в ответе несколько событий, сначала разберите каждое. Более высокий offset подтверждает предыдущие обновления, как описано в getUpdates.

Токен во всех примерах передаётся curl через стандартный ввод. Вставлять его в адресную строку браузера или в текст статьи не требуется. Каждый ответ сначала прочитайте: при ok:false разберите description, после обрыва проверьте собственный чат, прежде чем повторять отправку.

Отправьте reply-кнопки и получите обычный текст

Сохраните обе заготовки в переменных. Они понадобятся и для reply, и для callback.

PLAN='План встречи. Цель встречи; решение, которое нужно принять; следующий шаг и ответственный.'
QUESTIONS='Вопросы к встрече. Что мешает продвинуть задачу? Какие данные уже есть? Какое решение нужно принять?'
printf 'url = "https://api.telegram.org/bot%s/sendMessage"\n' "$BOT_TOKEN" |
  curl --silent --show-error --max-time 20 --config - --request POST \
    --data-urlencode "chat_id=$CHAT_ID" \
    --data-urlencode 'text=Что подготовить к встрече?' \
    --data-urlencode 'reply_markup={"keyboard":[[{"text":"План"},{"text":"Вопросы"}]],"resize_keyboard":true}'

При ok:true в личном чате ожидается вопрос с клавиатурой рядом с полем ввода. Нажмите «План», затем повторите блок getUpdates. Найдите новое сообщение и сверьте message.chat.id с CHAT_ID, message.from.id с OWNER_ID, а message.text со словом «План».

Для такого ответа выберите ANSWER="$PLAN". Если нажали «Вопросы», выберите ANSWER="$QUESTIONS". Сам по себе присланный текст ещё не доказывает нажатия: такую же реплику человек может набрать вручную. Для нашего выбора формата это подходит.

Отправьте выбранную заготовку и уберите reply-клавиатуру:

ANSWER="$PLAN"
printf 'url = "https://api.telegram.org/bot%s/sendMessage"\n' "$BOT_TOKEN" |
  curl --silent --show-error --max-time 20 --config - --request POST \
    --data-urlencode "chat_id=$CHAT_ID" \
    --data-urlencode "text=$ANSWER" \
    --data-urlencode 'reply_markup={"remove_keyboard":true}'

Первая строка соответствует выбранному «Плану»; для второго варианта замените её на ANSWER="$QUESTIONS". После успешного ответа ожидается текст заготовки и исчезновение клавиатуры. Обновите OFFSET по обработанным событиям.

Прикрепите callback- и URL-кнопки к вопросу

Теперь отправьте тот же вопрос с кнопками под сообщением:

printf 'url = "https://api.telegram.org/bot%s/sendMessage"\n' "$BOT_TOKEN" |
  curl --silent --show-error --max-time 20 --config - --request POST \
    --data-urlencode "chat_id=$CHAT_ID" \
    --data-urlencode 'text=Что подготовить к встрече?' \
    --data-urlencode 'reply_markup={"inline_keyboard":[[{"text":"План","callback_data":"prep:plan"},{"text":"Вопросы","callback_data":"prep:questions"}],[{"text":"Справка о кнопках","url":"https://core.telegram.org/bots/features#keyboards"}]]}' \
    --output inline-sent.json
cat inline-sent.json

Из успешного ответа возьмите result.message_id. Сохраните его как номер текущего вопроса:

read -r -p 'result.message_id вопроса: ' QUESTION_ID

Рядом запишите CHAT_ID, OWNER_ID, QUESTION_ID и состояние «Выбор открыт». Эти сведения связывают будущий callback с конкретным вопросом и человеком.

Видимые подписи здесь короткие, а служебные значения различаются: prep:plan и prep:questions. В InlineKeyboardButton для callback_data указан предел от 1 до 64 байт. Для каждой из наших кнопок задан один способ действия, callback_data либо url.

Сначала нажмите «Справка о кнопках». Должна открыться страница Telegram Bot Features. Затем вернитесь в чат. У URL-кнопки своей callback-ветви нет, поэтому отсутствие callback_query для перехода ожидаемо. Из этого же следует полезная граница: по одному Bot API-событию факт чтения страницы не установить.

Подтвердите callback и замените вопрос заготовкой

Подготовьте следующие команды заранее, затем нажмите «План» под сообщением и повторите блок getUpdates. В новом событии ищите объект callback_query. Перед ответом сверьте четыре вещи:

  • from.id совпадает с OWNER_ID;
  • message.chat.id совпадает с CHAT_ID;
  • message.message_id совпадает с QUESTION_ID;
  • data содержит prep:plan либо prep:questions, а выбор в записи ещё открыт.

При совпадении полей задайте NOTICE='Выбор получен'. Если поля отсутствуют или расходятся с записью, задайте NOTICE='Этот выбор недоступен': выполните только подтверждение следующим запросом, оставив сообщение и состояние прежними. Само знакомое значение data права на действие не даёт.

У одного нажатия есть собственный callback_query.id. Введите его строковое значение целиком, затем отправьте подтверждение:

read -r -p 'callback_query.id: ' CALLBACK_ID
printf 'url = "https://api.telegram.org/bot%s/answerCallbackQuery"\n' "$BOT_TOKEN" |
  curl --silent --show-error --max-time 20 --config - --request POST \
    --data-urlencode "callback_query_id=$CALLBACK_ID" \
    --data-urlencode "text=$NOTICE"

При успехе ожидается ok:true и result:true. Подтверждение убирает индикатор ожидания у нажатой кнопки; текст вопроса остаётся прежним. Эту отдельную роль answerCallbackQuery показывает и учебник Telegram.

После успешного подтверждения допустимого открытого выбора для prep:plan выберите ANSWER="$PLAN", для prep:questions задайте ANSWER="$QUESTIONS". Затем измените исходное сообщение, одновременно убрав кнопки выбора:

ANSWER="$PLAN"
printf 'url = "https://api.telegram.org/bot%s/editMessageText"\n' "$BOT_TOKEN" |
  curl --silent --show-error --max-time 20 --config - --request POST \
    --data-urlencode "chat_id=$CHAT_ID" \
    --data-urlencode "message_id=$QUESTION_ID" \
    --data-urlencode "text=$ANSWER" \
    --data-urlencode 'reply_markup={"inline_keyboard":[]}'

Первая строка снова соответствует «Плану». В успешном ответе editMessageText проверьте номер сообщения и новый текст, затем посмотрите сам чат. Вопрос должен превратиться в выбранную заготовку, кнопки под ним исчезнут. В записи поменяйте состояние на «Выбрано: План» либо «Выбрано: Вопросы». Только после обработки полученных событий продвиньте OFFSET.

Здесь легко перепутать идентификаторы. callback_query.id нужен для подтверждения нажатия, message_id указывает сообщение для правки, from.id определяет человека. А update_id помогает продолжить чтение очереди. Подставлять один вместо другого бессмысленно, даже если все они похожи на длинные числа.

Проверьте повтор до переноса в программу

Для проверки второго варианта отправьте новый inline-вопрос, возьмите его новый message_id, откройте выбор в записи и повторите проход с «Вопросами». Идентификатор прежнего вопроса уже относится к завершённому выбору.

Затем на новом вопросе быстро нажмите одну callback-кнопку дважды, пока клавиатура ещё видна. Разберите полученные события по очереди. Первое допустимое нажатие закрывает выбор. Если пришёл ещё один callback для этого же вопроса, задайте NOTICE='Выбор уже сохранён' и подтвердите его отдельным answerCallbackQuery. Повторно менять выбранную заготовку при этом незачем.

Повторно полученный update_id отмечайте как уже обработанный. Новое нажатие может иметь другой идентификатор, поэтому одной проверки очереди для закрытого вопроса мало. Наше правило для этого примера, первый принятый выбор сохраняется, относится к паре chat_id и message_id вместе с разрешённым отправителем.

У ручной записи есть ясная задача: зафиксировать такое поведение до переноса в постоянный обработчик. В программе очередь, состояние выбора и защита от повторного действия потребуют своего хранения. Особенно если вместо показа заготовки кнопка оформляет заявку или запускает внешнюю операцию.

После завершения закройте доступ к токену в текущем окне:

unset BOT_TOKEN

Вопросы и ответы

Кнопка крутится, хотя заготовка уже появилась. Что пропущено?

Проверьте ответ на answerCallbackQuery. Изменение сообщения и подтверждение нажатия выполняют разные задачи. Для подтверждения требуется актуальный callback_query.id; номер сообщения сюда не подходит. Если API сообщает, что запрос устарел, начните новый учебный выбор и обработайте свежее нажатие сразу после получения.

getUpdates возвращает конфликт или пустой result. Куда смотреть?

При конфликте проверьте webhook и другой получатель обновлений. Для ручного прохода нужен один получатель у отдельного учебного бота. При пустом массиве отправьте ему новое сообщение в личном чате, сверьте бота и OFFSET, затем выполните чтение после этого действия. Пустой ответ сам по себе ещё не объясняет причину.

Можно задать цвет кнопке?

В текущем Bot API у кнопки есть поле style с готовыми вариантами primary, success и danger, синим, зелёным и красным. Произвольный HEX-цвет этим полем не задаётся; при отсутствии style оформление выбирает приложение. Для наших нейтральных вариантов поле опущено. Вид проверьте в тех клиентах Telegram, которыми пользуются участники.

Можно просто добавить кнопки через BotFather?

Для этого сценария разметку передаёт sendMessage, а результат нажатия обрабатывает получатель событий. BotFather регистрирует бота и управляет его настройками. Появление кнопок и работа выбора в нашем проходе проверяются отдельными действиями.

Сохраните тексты обеих заготовок, разметку клавиатуры и правило закрытого выбора. Полезный результат этого разбора виден в чате: reply даёт текстовую реплику, URL открывает справку, а допустимый callback подтверждается и заменяет свой вопрос нужной заготовкой.

Для следующей версии перенесите тот же выбор в карту сценария и испытаний. В поле «Одно действие бота» укажите подготовку плана или вопросов к встрече, во входе запишите два callback-значения, а в неверном входе и отказе сохраните чужого отправителя, старый вопрос и повторное нажатие. В «Первопроходцах» можно задавать вопросы по собственной работе; проектные работы Никита Малов проверяет лично и даёт правки. Собственные боты относятся к позднему этапу программы, уроки открываются постепенно.

Документация Telegram сверена 19 октября 2026 года. Диалог и ожидаемые результаты учебные; обложка создана с помощью ImageGen.