This guide demonstrates how to handle Telegram InlineKeyboardMarkup and callback_query updates using standard PHP. We will construct inline keyboards, answer incoming callback queries, enforce the 64-byte payload constraint, and update the originating message in-place using editMessageText.
This article focuses purely on low-level HTTP interaction with the Telegram Bot API and payload parsing. It does not cover framework abstractions or long-polling daemons.
The 64-Byte Limit and Compact Payloads
Telegram restricts callback_data strings to a maximum of 64 bytes. Attempting to pass complex JSON objects inside button payloads will trigger API errors or fail silently when truncated.
To pass state efficiently:
- Use short prefix keys separated by a delimiter (e.g.,
act:id->app:8492). - Never encode full database records inside
callback_data. Store multi-step state on the server (Redis, SQL) and pass only the entity ID or temporary random token.
Step 1: Execute Requests with Strict HTTP Checking
All API calls must handle potential cURL failure, verify non-200 HTTP response codes, confirm valid JSON decoding, and check Telegram's top-level ok boolean flag.
Here is a cURL helper function written for standard PHP:
function callTelegramApi(string $method, array $payload = []): array
{
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
throw new Exception('TELEGRAM_BOT_TOKEN environment variable is missing.');
}
$url = "https://api.telegram.org/bot{$token}/{$method}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("cURL request failed: {$curlError}");
}
if ($httpCode < 200 || $httpCode >= 300) {
throw new Exception("HTTP request failed with status code {$httpCode}: {$response}");
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('JSON parsing error: ' . json_last_error_msg());
}
if (!isset($data['ok']) || $data['ok'] !== true) {
$description = $data['description'] ?? 'Unknown error';
throw new Exception("Telegram API returned an error: {$description}");
}
return $data['result'];
}
Step 2: Send a Message with an Inline Keyboard
An inline keyboard attaches directly to a message using the reply_markup payload. Buttons inside inline keyboards send a callback_query to your webhook when pressed by a user.
function sendApprovalRequest(int $chatId, string $entityId): array
{
$keyboard = [
'inline_keyboard' => [
[
['text' => 'Approve', 'callback_data' => "app:{$entityId}"],
['text' => 'Reject', 'callback_data' => "rej:{$entityId}"]
]
]
];
return callTelegramApi('sendMessage', [
'chat_id' => $chatId,
'text' => htmlspecialchars("Request #{$entityId} requires approval.", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
'parse_mode' => 'HTML',
'reply_markup' => $keyboard
]);
}
Step 3: Handle Callback Queries and Update UI
When a user taps an inline button, Telegram sends an Update containing a callback_query. You must address this event in two phases:
- Call
answerCallbackQueryimmediately. This removes the loading spinner on the user's Telegram client. - Execute business logic and update the original message via
editMessageTextto reflect the changes.
$rawInput = file_get_contents('php://input');
$update = json_decode($rawInput, true);
if (isset($update['callback_query'])) {
$callbackQuery = $update['callback_query'];
$callbackId = $callbackQuery['id'];
$callbackData = $callbackQuery['data'] ?? '';
$message = $callbackQuery['message'];
$chatId = $message['chat']['id'];
$messageId = $message['message_id'];
// Enforce payload length safeguard (64 bytes maximum)
if (strlen($callbackData) > 64) {
callTelegramApi('answerCallbackQuery', [
'callback_query_id' => $callbackId,
'text' => 'Invalid action payload.',
'show_alert' => true
]);
exit;
}
// Acknowledge the query to stop client spinner
callTelegramApi('answerCallbackQuery', [
'callback_query_id' => $callbackId,
'text' => 'Processing request...'
]);
// Parse compact payload format 'action:entityId'
$parts = explode(':', $callbackData, 2);
$action = $parts[0] ?? '';
$entityId = $parts[1] ?? '';
if ($action === 'app') {
$statusText = "Request #{$entityId} was <b>Approved</b>.";
} elseif ($action === 'rej') {
$statusText = "Request #{$entityId} was <b>Rejected</b>.";
} else {
$statusText = "Unknown action executed.";
}
// Update the existing message text and remove buttons
callTelegramApi('editMessageText', [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => $statusText,
'parse_mode' => 'HTML'
]);
}
Production Considerations
-
Byte vs Character Length: PHP's
strlen()measures byte length rather than character count, matching Telegram's 64-byte payload limit requirement. -
Message State Updates: Calling
editMessageTextwith content identical to the current message will throw a400 Bad Request: message is not modifiederror from Telegram. Check status server-side before attempting an update. -
Payload Security: Do not trust data inside
callback_datafor authorized actions without checking user permissions ($callbackQuery['from']['id']) against backend access rules.
If you need assistance scaling custom Telegram infrastructure or web app integrations, BotCreator is a engineering studio that ships production Telegram bots and Mini Apps.
Top comments (0)