Keyboards

Inline keyboards

How to create and dynamically change Inline keyboards in Telegram bots on PHP. Limitations of callback_data and examples of sending JSON requests.

Inline Keyboard (built-in keyboard) — this is a grid of interactive buttons that attaches directly to a specific message. Unlike regular Reply keyboards (which replace the standard user keyboard at the bottom of the screen), inline buttons do not clutter the input interface, move with the message when forwarded, and can change dynamically "on the fly" without sending new messages.

There are two main use cases for such buttons: instant transition via an external link (buttons of type url) and sending a hidden request to your server (buttons of type callback_data). The second option turns the bot into a fully interactive application with instant response.

Anatomy of the reply_markup parameter

To send a keyboard, the optional parameter reply_markup is used in method sendMessage (or other media-sending methods). This parameter receives a JSON string describing the keyboard object.

The key field here is inline_keyboard. It is a two-dimensional array (array of arrays). The outer array represents button rows, and inner arrays represent individual buttons in that row (columns). This structure allows flexible grid configuration: for example, create one large button spanning full width, and under it two buttons side by side.

Each button is an associative array containing mandatory text (text) and one of the action parameters. The most popular ones:

  • callback_data — a string up to 64 bytes that will be returned to your bot upon pressing.
  • url — a classic link for browser navigation.
  • web_app — a link to a Mini App that opens directly within Telegram.

We use function telegramApi() which we created in chapter Первый запрос: getMe. Here we only call it — we don't copy the implementation.

<?php
// Отправка сообщения с инлайн-клавиатурой
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
exit("Ошибка: TELEGRAM_BOT_TOKEN не задан в окружении\n");
}

$chatId = 123456789; // Замените на реальный chat_id пользователя
$leadId = bin2hex(random_bytes(4)); // Генерация уникального ID для примера

// Формируем структуру клавиатуры
$keyboard = [
'inline_keyboard' => [
// Первая строка кнопок (две кнопки в ряд)
[
['text' => '✅ Взять в работу', 'callback_data' => 'take_lead:' . $leadId],
['text' => '❌ Отклонить', 'callback_data' => 'reject_lead:' . $leadId],
],
// Вторая строка кнопок (одна кнопка на всю ширину)
[
['text' => '🌐 Перейти на сайт', 'url' => 'https://example.com/leads/' . $leadId],
]
]
];

$params = [
'chat_id' => $chatId,
'text' => "<b>Новая заявка #{$leadId}</b>\nСтатус: Ожидает обработки",
'parse_mode' => 'HTML',
'reply_markup' => json_encode($keyboard) // Обязательно кодируем в JSON!
];

$response = telegramApi($token, 'sendMessage', $params);

// Выводим результат для отладки в терминале
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL;

Example of successful API response

If the request executes successfully, the Telegram Bot API returns a JSON response containing object Message with a filled-in field reply_markup:

{
"ok": true,
"result": {
"message_id": 1502,
"from": {
"id": 987654321,
"is_bot": true,
"first_name": "My Helper Bot",
"username": "my_helper_bot"
},
"chat": {
"id": 123456789,
"first_name": "Иван",
"type": "private"
},
"date": 1710000000,
"text": "Новая заявка #a1b2c3d4\nСтатус: Ожидает обработки",
"reply_markup": {
"inline_keyboard": [
[
{
"text": "✅ Взять в работу",
"callback_data": "take_lead:a1b2c3d4"
},
{
"text": "❌ Отклонить",
"callback_data": "reject_lead:a1b2c3d4"
}
],
[
{
"text": "🌐 Перейти на сайт",
"url": "https://example.com/leads/a1b2c3d4"
}
]
]
}
}
}

Note the field message_id (in the example — 1502). It must be saved in your database if you plan to later modify this keyboard or message text when processing button clicks.

Strict limitation on callback_data

The most common beginner mistake is trying to pack too much information into field callback_data (for example, a JSON string with product parameters). Telegram imposes a strict limit: the value of callback_data must not exceed 64 bytes.

If you try to send a button with data larger than 64 bytes, the API will return error Bad Request: BUTTON_DATA_INVALID. To avoid this, use compact formats:

  • Shorten action names to the minimum (instead of approve_registration_request write appr_reg).
  • Use delimiters (for example, a colon :) to separate the action and resource ID: take:452.
  • Store all heavy data (state, complete request parameters) in your database on your server, and pass only the primary key (record ID) to the button.

Dynamic keyboard modification

The bot's interface looks professional when after pressing a button it changes to another or disappears entirely. This prevents repeated accidental presses (for example, when two managers try to take the same ticket).

To modify only the button grid without changing the message text itself, method editMessageReplyMarkup is used. If both text and buttons need updating simultaneously, use method editMessageText, which we discussed in chapter Редактирование и удаление.

We use function telegramApi() which we created in chapter Первый запрос: getMe. Here we only call it — we don't copy the implementation.

<?php
// Удаление клавиатуры или замена её на новую
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
exit("Ошибка: TELEGRAM_BOT_TOKEN не задан в окружении\n");
}

$chatId = 123456789;
$messageId = 1502; // ID сообщения, полученный при отправке

// Чтобы полностью удалить клавиатуру, передаем пустой массив inline_keyboard.
// Чтобы изменить кнопки — передаем новую структуру.
$newKeyboard = [
'inline_keyboard' => [
[
['text' => '🔒 Заявка обработана', 'callback_data' => 'done']
]
]
];

$params = [
'chat_id' => $chatId,
'message_id' => $messageId,
'reply_markup' => json_encode($newKeyboard)
];

$response = telegramApi($token, 'editMessageReplyMarkup', $params);

// Проверяем результат выполнения
if (isset($response['ok']) && $response['ok'] === true) {
echo "Клавиатура успешно обновлена!\n";
} else {
echo "Ошибка обновления: " . ($response['description'] ?? 'Неизвестная ошибка') . "\n";
}

How to debug API responses in the terminal

When working with keyboards, markup errors (incorrect array nesting) often occur. To quickly understand what went wrong, always output the result of calling function telegramApi(). In CLI scripts, it's convenient to use the following construct:

echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

If the request failed, you will see the following structure:

{
"ok": false,
"error_code": 400,
"description": "Bad Request: message is not modified: specified new reply markup is an exact copy of the current one"
}

This specific error means you tried to set exactly the same keyboard that is already displayed to the user. The Telegram Bot API considers this redundant action and returns error 400.

Other inline button types

Besides callback_data and url, the Bot API supports specialized button types:

  • Switch inline query (parameters switch_inline_query or switch_inline_query_current_chat) — switches the user to inline mode of interaction with your bot, automatically inserting his username into the input field.
  • Web App (parameter web_app) — opens a full-featured web application directly inside Telegram. We'll discuss this mechanism in detail in chapter Mini Apps.

The practical application of this scheme is detailed in project Заявка с сайта → Telegram.

Next: callback_query — обрабатываем нажатия inline-кнопок

"}