Клавиатуры

callback_query

Разбираем обработку callback_query в Telegram Bot API на PHP. Как правильно отвечать на нажатия инлайн-кнопок, обходить лимит в 64 байта и избегать зависания интерфейса.

Когда пользователь нажимает инлайн-кнопку с параметром callback_data, Telegram не отправляет текстовое сообщение в чат. Вместо этого ваш бот получает объект Update, внутри которого вместо привычного ключа message находится ключ callback_query. Это совершенно другой тип события, который требует особого жизненного цикла обработки.

В отличие от обычных текстовых сообщений, нажатие инлайн-кнопки ожидает от бота явного подтверждения. Если этого не сделать, интерфейс у пользователя «зависнет» с крутящейся анимацией загрузки.

Как увидеть callback_query изнутри

В главе Webhook мы подробно разбирали, как принимать входящие запросы от Telegram. Чтобы детально изучить структуру нажатия кнопки, запишите сырой поток ввода в JSON-файл на вашем сервере. Это самый надежный способ отладки без использования сторонних дамперов.

<?php
// log.php — скрипт для отладки входящих запросов
$rawInput = file_get_contents('php://input');
if (!empty($rawInput)) {
file_put_contents(__DIR__ . '/callback_log.json', $rawInput . PHP_EOL, FILE_APPEND);
}

После того как пользователь нажмет на кнопку, созданную по правилам из главы Inline Keyboard, в файле callback_log.json появится структура следующего вида:

{
"update_id": 987654321,
"callback_query": {
"id": "438210987654321012",
"from": {
"id": 123456789,
"is_bot": false,
"first_name": "Иван",
"username": "ivan_dev",
"language_code": "ru"
},
"message": {
"message_id": 4501,
"from": {
"id": 999999999,
"is_bot": true,
"first_name": "My Test Bot",
"username": "my_test_bot"
},
"chat": {
"id": 123456789,
"first_name": "Иван",
"username": "ivan_dev",
"type": "private"
},
"date": 1700000000,
"text": "Выберите действие:",
"reply_markup": {
"inline_keyboard": [
[
{
"text": "✅ Принять",
"callback_data": "order_approve:8472"
}
]
]
}
},
"chat_instance": "-81273981273981273",
"data": "order_approve:8472"
}
}

Разбор ключевых полей callback_query

  • id — уникальный идентификатор самого нажатия (строка). Он критически важен: его необходимо передать в метод answerCallbackQuery, чтобы подтвердить получение клика и убрать анимацию загрузки на кнопке.
  • from — объект пользователя, совершившего нажатие. Обратите внимание: в группах на кнопку может нажать любой участник, а не только тот, кому бот адресовал сообщение. Проверяйте этот ID, если кнопка персональная.
  • message — объект оригинального сообщения, под которым находилась клавиатура. Из него мы извлекаем chat.id и message_id для последующего редактирования или удаления сообщения.
  • data — строковое значение, которое было зашито в кнопку (в примере: order_approve:8472). Именно на основе этой строки ваш PHP-скрипт определяет, какое действие нужно выполнить.

Обязательное правило: Метод answerCallbackQuery

Когда пользователь нажимает инлайн-кнопку, в его Telegram-клиенте на кнопке появляется анимированная иконка часов («часики»). Она крутится до тех пор, пока бот не подтвердит обработку нажатия.

Если бот не ответит на запрос в течение 10–30 секунд, иконка исчезнет сама, но пользователь увидит неприятное предупреждение о том, что бот не отвечает. Чтобы этого избежать, вы обязаны вызвать метод answerCallbackQuery.

Параметры метода answerCallbackQuery:

  • callback_query_id (обязательный) — строка id из пришедшего апдейта.
  • text (опциональный) — текст уведомления, которое увидит пользователь (до 200 символов).
  • show_alert (опциональный) — если передать true, Telegram покажет полноценное модальное окно с кнопкой «ОК». Если false (по умолчанию), текст появится как быстро исчезающая плашка (тост) вверху экрана.

Практический пример обработчика на PHP

Для тестирования обработчика локально без отправки реальных запросов от Telegram, вы можете сымитировать входящий клик. Для этого выполните следующую команду в терминале, подставив URL вашего локального сервера:

curl -X POST -H "Content-Type: application/json" \
-d '{"update_id":987654321,"callback_query":{"id":"test_click_123","data":"order_approve:8472","message":{"chat":{"id":123456789},"message_id":4501}}}' \
http://localhost/index.php

Используем функцию telegramApi(), которую мы сделали в главе Первый запрос: getMe. Здесь только вызываем её — реализацию не копируем.

<?php
// index.php — обработчик Webhook

require_once __DIR__ . '/bot-helper.php'; // Подключаем функцию telegramApi()

$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
exit('Token not found');
}

$input = file_get_contents('php://input');
$update = json_decode($input, true);

if (isset($update['callback_query'])) {
$callbackQuery = $update['callback_query'];
$callbackId = $callbackQuery['id'];
$data = $callbackQuery['data'];

$chatId = $callbackQuery['message']['chat']['id'] ?? null;
$messageId = $callbackQuery['message']['message_id'] ?? null;

// Разбираем payload формата "действие:параметр"
$parts = explode(':', $data, 2);
$action = $parts[0] ?? '';
$payload = $parts[1] ?? '';

if ($action === 'order_approve') {
$orderId = (int)$payload;

// 1. Сразу убираем «часики» и показываем всплывающее уведомление
telegramApi($token, 'answerCallbackQuery', [
'callback_query_id' => $callbackId,
'text' => "Заказ #{$orderId} успешно принят!",
'show_alert' => false
]);

// 2. Редактируем сообщение, удаляя клавиатуру, чтобы избежать повторных нажатий
// Подробнее о редактировании сообщений читайте в главе "Редактирование и удаление"
if ($chatId && $messageId) {
telegramApi($token, 'editMessageText', [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => "✅ Заказ #{$orderId} обработан менеджером.",
'reply_markup' => [
'inline_keyboard' => [] // Передаём пустую сетку для удаления кнопок
]
]);
}
} else {
// Обработка неизвестного действия
telegramApi($token, 'answerCallbackQuery', [
'callback_query_id' => $callbackId,
'text' => 'Неизвестное действие.',
'show_alert' => true
]);
}
}

После успешного вызова метода answerCallbackQuery Telegram возвращает стандартный JSON-ответ:

{
"ok": true,
"result": true
}

Если вы получили ok: false, обратите внимание на поле description. Самая частая ошибка — Bad Request: query is too old and response timeout expired. Это означает, что с момента нажатия кнопки пользователем до вызова метода вашим PHP-скриптом прошло более 10-30 секунд (например, из-за долгого выполнения скрипта или зависшего запроса к БД).

Проектирование callback_data и лимит в 64 байта

Самая частая ошибка новичков — попытка упаковать в параметр callback_data сложные JSON-объекты, длинные пути или текстовые описания. Telegram накладывает жёсткое ограничение в 64 байта на значение поля callback_data.

Если вы превысите этот лимит при отправке клавиатуры (метод sendMessage), API вернёт ошибку Bad Request: BUTTON_DATA_INVALID, и сообщение вообще не будет отправлено пользователю.

Как уложиться в лимит:

  1. Используйте короткие префиксы. Вместо длинного approve_delivery_order пишите ok_dlv или просто y. Разделителем обычно выступает двоеточие : или точка с запятой ;.
  2. Передавайте только ID. Не пытайтесь передать имя товара, цену или дату. Передавайте только первичный ключ из базы данных (например, id:1482). Все остальные данные ваш PHP-скрипт должен извлечь из БД по этому ID.
  3. Помните про UTF-8. Кириллические символы занимают от 2 до 4 байт каждый. Латиница и цифры занимают строго 1 байт. Всегда пишите технический payload в callback_data только латинскими буквами и цифрами.

Подводные камни при работе с callback_query

  • Повторные нажатия (Race Conditions). Пользователь может очень быстро нажать на одну и ту же кнопку несколько раз подряд. Telegram отправит вашему боту несколько идентичных апдейтов callback_query параллельно. Ваш PHP-код должен проверять текущее состояние сущности в БД. Если заказ уже обработан, первый клик выполнит действие, а на второй клик бот должен просто ответить через answerCallbackQuery с текстом «Этот заказ уже обработан» без повторного изменения БД.
  • Устаревание данных. Сообщение с кнопками может висеть в чате неделями. Если пользователь нажмёт кнопку через месяц, а запись в вашей БД уже удалена, скрипт выдаст ошибку. Всегда проверяйте существование записи в БД перед выполнением бизнес-логики.
  • Отсутствие message в inline-режиме. Если клавиатура была отправлена через Inline Mode (когда бот работает в чужом чате через инлайн-запросы), в объекте callback_query не будет поля message. Вместо него придёт поле inline_message_id. Для редактирования такого сообщения в методе editMessageText нужно указывать параметр inline_message_id вместо пары chat_id + message_id.

Дальше: Reply Keyboard — классические кнопки под полем ввода текста и их отличия от инлайн-кнопок.