Keyboards
callback_query
We analyze handling of callback_query in Telegram Bot API on PHP. How to correctly respond to inline button presses, bypass the 64-byte limit, and avoid interface freezing.
When a user presses an inline button with parameter callback_data, Telegram does not send a text message in the chat. Instead, your bot receives an object Update, inside which instead of the usual key message there is the key callback_query. This is a completely different type of event that requires special lifecycle handling.
Unlike regular text messages, pressing an inline button expects the bot to provide explicit confirmation. If this is not done, the user interface will "freeze" with a spinning loading animation.
How to see callback_query from within
In chapter Webhook we thoroughly examined how to receive incoming requests from Telegram. To study the button click structure in detail, save the raw input stream to a JSON file on your server. This is the most reliable way to debug without using third-party dumpers.
<?php
// log.php — скрипт для отладки входящих запросов
$rawInput = file_get_contents('php://input');
if (!empty($rawInput)) {
file_put_contents(__DIR__ . '/callback_log.json', $rawInput . PHP_EOL, FILE_APPEND);
}
After the user presses the button created according to the rules in chapter Inline Keyboard, the structure in file callback_log.json will appear in the following form:
{
"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"
}
}
Breakdown of callback_query key fields
id— unique identifier of the click itself (string). It is critically important: you must pass it to methodanswerCallbackQueryto confirm receipt of the click and remove the loading animation on the button.from— the user object that performed the click. Note: in groups any participant can press the button, not just the one who received the message. Check this ID if the button is personal.message— the original message object under which the keyboard was located. From it we extractchat.idandmessage_idfor subsequent editing or deletion of the message.data— the string value that was embedded in the button (in the example:order_approve:8472). Based on this string, your PHP script determines what action needs to be taken.
Mandatory rule: The answerCallbackQuery method
When a user presses an inline button, an animated hourglass icon appears on the button in their Telegram client. It spins until the bot confirms processing of the click.
If the bot does not respond to the request within 10–30 seconds, the icon will disappear automatically, but the user will see an unpleasant warning that the bot is not responding. To avoid this, you must call method answerCallbackQuery.
Parameters of method answerCallbackQuery:
callback_query_id(required) — the stringidfrom the incoming update.text(optional) — notification text that the user will see (up to 200 characters).show_alert(optional) — if you passtrue, Telegram will show a full modal window with an OK button. Iffalse(default), the text will appear as a quickly disappearing toast at the top of the screen.
Practical example of a PHP handler
To test the handler locally without sending real requests from Telegram, you can simulate an incoming click. For this, execute the following command in your terminal, substituting your local server 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
We use function telegramApi() which we created in chapter Первый запрос: getMe. Here we only call it — we do not copy the implementation.
<?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
]);
}
}
After a successful call to method answerCallbackQuery Telegram returns the standard JSON response:
{
"ok": true,
"result": true
}
If you received ok: false, pay attention to field description. A common error is Bad Request: query is too old and response timeout expired. This means that more than 10–30 seconds have passed since the user pressed the button before calling your PHP script. For example, due to long script execution time or a hanging database query.
Designing callback_data and the 64-byte limit
The most common mistake of beginners is trying to pack complex JSON objects, long paths, or text descriptions into parameter callback_data. Telegram imposes a strict **64-byte limit** on the value of field callback_data.
If you exceed this limit when creating the keyboard (method sendMessage), the API will return error Bad Request: BUTTON_DATA_INVALID, and the message will not be sent to the user at all.
How to stay within the limit:
- Use short prefixes. Instead of a long
approve_delivery_orderwriteok_dlvor simplyy. A separator is usually a colon:or a semicolon;. - Pass only IDs. Do not try to pass product name, price, or date. Pass only the primary key from the database (for example,
id:1482). Your PHP script should retrieve all other data from the DB using this ID. - Remember about UTF-8. Cyrillic characters occupy 2 to 4 bytes each. Latin letters and digits occupy strictly 1 byte. Always write technical payloads in
callback_datausing only Latin letters and numbers.
Pitfalls when working with callback_query
- Race conditions (Repeated clicks). A user may very quickly press the same button multiple times in quick succession. Telegram will send your bot multiple identical updates
callback_queryin parallel. Your PHP code must check the current state of the entity in the DB. If the order has already been processed, the first click will perform the action, and on the second click the bot should simply reply viaanswerCallbackQuerywith text "This order has already been processed" without updating the DB again. - Data staleness. The button message may linger in the chat for weeks. If the user presses the button a month later, but the record in your DB has already been deleted, the script will throw an error. Always check for the existence of a record in the DB before executing business logic.
- Missing message in inline mode. If the keyboard was sent via Inline Mode (when the bot works in another chat through inline queries), the object
callback_querywill not contain fieldmessage. Instead, fieldinline_message_idwill come. For editing such a message in methodeditMessageTextyou need to specify parameterinline_message_idinstead of the pairchat_id+message_id.
Next: Reply Keyboard — classic buttons below a text input field and their differences from inline buttons.