DEV Community

Serhii
Serhii

Posted on Originally published at botservice.biz

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'];
}
Enter fullscreen mode Exit fullscreen mode

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
    ]);
}
Enter fullscreen mode Exit fullscreen mode

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'
    ]);
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)