При создании интерактивных ботов в Telegram ключевым инструментом взаимодействия с пользователем выступают инлайн-кнопки (InlineKeyboardMarkup). В отличие от обычных клавиатур (ReplyKeyboardMarkup), нажатие на инлайн-кнопку генерирует событие callback_query, которое отправляется на ваш Webhook без засорения чата текстовыми сообщениями.
Однако при разработке логики смены экранов, фильтров или выбора товаров разработчики регулярно сталкиваются с жесткими техническими ограничениями Telegram API: строго лимитированным размером поля callback_data, поддержкой идемпотентности запросов и необходимостью немедленного подтверждения нажатия кнопки.
Лимит 64 байта в callback_data: почему нельзя передавать JSON
Главное архитектурное ограничение параметра callback_data в объекте InlineKeyboardButton — его размер не должен превышать 64 байта. Важно учитывать, что речь идет именно о байтах, а не о символах. В кодировке UTF-8 кириллические символы занимают по 2 байта, а специальные символы и emoji — до 4 байт.
Попытка передать структурированный JSON вроде {"action":"show_category","id":1042,"page":3} приведет к тому, что Telegram API вернет ошибку 400 Bad Request: BUTTON_DATA_INVALID. Ниже приведен пример корректной функции для отправки cURL-запросов к API с проверкой ошибок.
<?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'];
}
Архитектура коротких payload: префиксы и генератор хэшей
Для укладывания в лимит 64 байт применяют две стратегии:
- Префиксная схема с двоеточием или слэшем: подходит для простых действий. Например:
act:item:1042:3(гдеact— действие,item— сущность,1042— ID,3— страница). Это занимает около 16 байт. - Схема с временным хранилищем в БД (Payload Registry): если контекст кнопки слишком велик (сложные фильтры, длинные UUID, предварительный состав заказа), в
callback_dataпередают короткий уникальный идентификатор, а данные хранят в PostgreSQL/MySQL или Redis.
Ниже пример создания короткого ключа записи для формы заявки с использованием криптографически стойкого идентификатора 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, Secret Token и идемпотентность
При обработке входящих обновлений от Telegram необходимо строго проверять заголовок X-Telegram-Bot-Api-Secret-Token, заданный при вызове setWebhook. Это защищает вашу конечную точку от поддельных HTTP-запросов.
Кроме того, Telegram гарантирует доставку обновлений по принципу «at least once» (хотя бы один раз). Из-за сетевых сбоев один и тот же update_id может прийти повторно. Чтобы не выполнять списывание средств или дублирование заявок дважды, обработанные update_id следует сохранять в базе данных и проверять перед запуском бизнес-логики.
Обязательный вызов answerCallbackQuery и бесшовный UX с editMessageText
При нажатии на инлайн-кнопку на клиенте пользователя появляется индикатор загрузки (крутящийся часики на кнопке). Если сервер не ответит вызовом метода answerCallbackQuery в течение 10 секунд, интерфейс зависнет, а Telegram на клиенте покажет ошибку тайм-аута.
Правильный жизненный цикл обработки события callback_query выглядит следующим образом:
- Валидировать секретный токен и спарсить входящий JSON.
- Проверить
update_idна предмет повторной обработки (идемпотентность). - Вызвать
answerCallbackQueryдля снятия индикатора загрузки на кнопке (можно передатьtextиshow_alert => trueдля всплывающего уведомления). - Обновить текущее сообщение через
editMessageTextилиeditMessageReplyMarkupдля отрисовки нового состояния интерфейса.
<?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']);
Типичные ошибки при работе с Inline-клавиатурами
- Игнорирование ошибок editMessageText: если попытаться отредактировать сообщение, передав точно такой же текст и
reply_markup, Telegram API вернет ошибку400 Bad Request: message is not modified. Логируйте отклики API и перехватывайте исключения. - Передача чувствительных данных в callback_data: помните, что
callback_dataне шифруется внутри инфраструктуры Telegram и может остаться в логах клиента или сервера. Не передавайте там токены авторизации или персональные данные. - Отсутствие обработки устаревших кнопок: пользователь может нажать на инлайн-кнопку в сообщении недельной давности. Система должна корректно обрабатывать ситуации, когда временный контекст в БД уже очищен по TTL.
Если вам требуется профессиональная интеграция сложных Telegram-ботов и Mini Apps с надежной архитектурой, закажите разработку у специалистов на BotCreator.