Manage Telegram Callback Queries and Inline Keyboards in PHP

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: 1. Call answerCallbackQuery immediately. This removes the loading spinner on the user's Telegram client. 2. Execute business logic and update the original message via editMessageText to 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 editMessageText with content identical to the current message will throw a 400 Bad Request: message is not modified error from Telegram. Check status server-side before attempting an update. * Payload Security: Do not trust data inside callback_data for 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.

New articles on Telegram

We explain what to automate in your business and how it works in practice. No spam.