When creating interactive Telegram bots, inline buttons (InlineKeyboardMarkup) serve as a key user interaction tool. Unlike regular keyboards (ReplyKeyboardMarkup), clicking an inline button generates a callback_query event that is sent to your Webhook without cluttering the chat with text messages.
However, when developing screen switching logic, filters, or product selection, developers regularly face strict technical limitations of the Telegram API: a strictly limited size of the callback_data field, request idempotency support, and the need for immediate button click confirmation.
64-byte limit in callback_data: why you shouldn't pass JSON
The main architectural limitation of the callback_data parameter in the InlineKeyboardButton object is that its size must not exceed 64 bytes. It is important to note that this refers specifically to bytes, not characters. In UTF-8 encoding, Cyrillic characters take up 2 bytes each, while special characters and emojis take up to 4 bytes.
Attempting to pass structured JSON like {\"action\":\"show_category\",\"id\":1042,\"page\":3} will cause the Telegram API to return a 400 Bad Request: BUTTON_DATA_INVALID error. Below is an example of a correct function for sending cURL requests to the API with error checking.
<?php
function sendTelegramApiRequest(string $method, array $params = []): array
{
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
throw new RuntimeException('TELEGRAM_BOT_TOKEN environment variable is not set.');
}
$url = "https://api.telegram.org/bot{$token}/{$method}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$response = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
throw new RuntimeException("cURL error during Telegram API call: {$curlError}");
}
$decoded = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Failed to parse JSON response from Telegram API.');
}
if ($httpCode !== 200 || !isset($decoded['ok']) || $decoded['ok'] !== true) {
$description = $decoded['description'] ?? 'Unknown error';
throw new RuntimeException("Telegram API error [HTTP {$httpCode}]: {$description}");
}
return $decoded['result'];
}
Short payload architecture: prefixes and hash generator
To stay within the 64-byte limit, two strategies are used:
- Prefix scheme with a colon or slash: suitable for simple actions. For example:
act:item:1042:3(whereactis the action,itemis the entity,1042is the ID, and3is the page). This takes about 16 bytes. - Database temporary storage scheme (Payload Registry): if the button context is too large (complex filters, long UUIDs, preliminary order details), a short unique identifier is passed in
callback_data, while the data is stored in PostgreSQL/MySQL or Redis.
Below is an example of creating a short record key for an application form using a cryptographically strong identifier lead_id.
<?php
function createLeadContext(PDO $pdo, int $userId, array $leadData): string
{
// Генерируем короткий уникальный ID длиною 14 символов (7 байт hex)
$leadId = bin2hex(random_bytes(7));
$stmt = $pdo->prepare('
INSERT INTO lead_contexts (lead_id, user_id, payload_data, created_at)
VALUES (:lead_id, :user_id, :payload_data, NOW())
');
$stmt->execute([
':lead_id' => $leadId,
':user_id' => $userId,
':payload_data' => json_encode($leadData, JSON_UNESCAPED_UNICODE),
]);
// Формируем callback_data, который занимает всего 18 байт: "confirm_lead:a1b2c3d4e5f6g7"
return "cnf_ld:{$leadId}";
}
Webhook validation, Secret Token, and idempotency
When processing incoming updates from Telegram, you must strictly verify the X-Telegram-Bot-Api-Secret-Token header set when calling setWebhook. This protects your endpoint from forged HTTP requests.
In addition, Telegram guarantees update delivery on an \"at least once\" basis. Due to network failures, the same update_id may be received repeatedly. To avoid charging funds or duplicating requests twice, processed update_id values should be stored in a database and checked before executing business logic.
Mandatory answerCallbackQuery call and seamless UX with editMessageText
When an inline button is clicked, a loading indicator (a spinning clock icon on the button) appears on the user's client. If the server does not respond with a call to the answerCallbackQuery method within 10 seconds, the UI will freeze, and Telegram on the client side will show a timeout error.
The correct lifecycle for processing a callback_query event is as follows:
- Validate секретный токен and parse incoming JSON.
- Check
update_idfor duplicate processing (idempotency). - Call
answerCallbackQueryto remove the loading indicator on the button (you can passtextandshow_alert => truefor a pop-up notification). - Update the current message via
editMessageTextoreditMessageReplyMarkupto render the new interface state.
<?php
// Входная точка обработки Webhook
$secretHeader = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
$expectedSecret = getenv('TELEGRAM_WEBHOOK_SECRET');
if (!hash_equals($expectedSecret, $secretHeader)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid secret token']);
exit;
}
$rawInput = file_get_contents('php://input');
$update = json_decode($rawInput, true);
if (!$update || !isset($update['update_id'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON update']);
exit;
}
$pdo = new PDO('mysql:host=127.0.0.1;dbname=bot_db;charset=utf8mb4', 'db_user', 'db_pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$updateId = (int)$update['update_id'];
// Проверка идемпотентности
$stmt = $pdo->prepare('INSERT IGNORE INTO processed_updates (update_id) VALUES (:update_id)');
$stmt->execute([':update_id' => $updateId]);
if ($stmt->rowCount() === 0) {
// Данный update_id уже был успешно обраборан ранее
http_response_code(200);
echo json_encode(['status' => 'already_processed']);
exit;
}
if (isset($update['callback_query'])) {
$callback = $update['callback_query'];
$callbackId = $callback['id'];
$callbackData = $callback['data'] ?? '';
$message = $callback['message'];
$chatId = $message['chat']['id'];
$messageId = $message['message_id'];
// 1. Снимаем лоадер с кнопки
sendTelegramApiRequest('answerCallbackQuery', [
'callback_query_id' => $callbackId,
'text' => 'Загрузка...',
'show_alert' => false,
]);
// 2. Разбираем сжатый payload
$parts = explode(':', $callbackData);
$action = $parts[0] ?? '';
if ($action === 'cnf_ld') {
$leadId = $parts[1] ?? '';
// Получаем контекст лида из БД
$stmtContext = $pdo->prepare('SELECT payload_data FROM lead_contexts WHERE lead_id = :lead_id');
$stmtContext->execute([':lead_id' => $leadId]);
$contextRow = $stmtContext->fetch();
if ($contextRow) {
$leadData = json_decode($contextRow['payload_data'], true);
// 3. Обновляем текст сообщения и менять клавиатуру
sendTelegramApiRequest('editMessageText', [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => "Заявка #{$leadId} успешно подтверждена!
Услуга: " . htmlspecialchars($leadData['service'] ?? 'Неуказана'),
'parse_mode' => 'HTML',
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '« Назад в меню', 'callback_data' => 'main_menu']
]
]
]
]);
}
}
}
http_response_code(200);
echo json_encode(['status' => 'ok']);
Common mistakes when working with Inline keyboards
- Ignoring editMessageText errors: if you attempt to edit a message by passing the exact same text and
reply_markup, the Telegram API will return a400 Bad Request: message is not modifiederror. Log API responses and catch exceptions. - Passing sensitive data in callback_data: remember that
callback_datais not encrypted within Telegram's infrastructure and may remain in client or server logs. Do not pass authorization tokens or personal data there. - Lack of handling for outdated buttons: a user might click an inline button in a week-old message. The system should correctly handle situations where the temporary context in the database has already been cleared by TTL.
If you need professional integration of complex Telegram-бот s and Mini Apps with a reliable architecture, order development from specialists at BotCreator.
"