When sending data from Telegram Mini App to your backend, the only single source of truth is the initData string. It is generated by the Telegram client application and signed with the bot's private key. If the data is validated incorrectly or if you trust the initDataUnsafe parameters from client-side JavaScript, an attacker will be able to spoof the user.id, bypass authentication, or perform actions on behalf of any user.
In this article, we will break down a secure algorithm for verifying initData in PHP, write an isolated validation function, and cover sending a callback to the Telegram API in compliance with error handling standards.
Anatomy of the initData String and Verification Algorithm
The initData string is a set of key=value pairs separated by an ampersand (&), similar to a standard URL query string. The parameters passed include query_id, user (a JSON string with profile data), auth_date (Unix timestamp), and the hash itself.
The authentication process consists of five required steps:
- Signature Extraction: the
hashparameter is separated from the received string. All remaining parameters are left to assemble the data check string. - Alphabetical Sorting: the remaining
key=valuepairs are sorted by key in alphabetical order (A-Z). - Constructing the data_check_string: the sorted pairs are joined together using a newline character (
). - HMAC Key Generation: a raw byte HMAC-SHA-256 hash of the string
"WebAppData"is created, using your bot token as the secret key. - Hash Comparison: a control HMAC-SHA-256 is calculated for the
data_check_stringusing the generated key. The result is compared with the receivedhashusing functions resistant to timing attacks.
Step-by-Step Implementation of Parsing and Validation in Pure PHP
When processing a query string, you cannot use basic parse_str() without preprocessing, as it decodes values and may modify the structure of the JSON inside the user parameter, breaking the final signature. It is more reliable to parse the string manually or work with raw URL-decoded values.
Below is a ready-to-use helper for authenticating initData with a signature lifetime limit (24 hours by default):
<?php
declare(strict_types=1);
/**
* Валидирует подпись initData от Telegram Mini App.
*
* @param string $initData Сырая строка window.Telegram.WebApp.initData
* @param string $botToken Токен бота из getenv() или конфига
* @param int $maxAge Максимально допустимый возраст подписи в секундах
* @return array|false Возвращает массив распарсенных данных или false при ошибке
*/
function validateTelegramInitData(string $initData, string $botToken, int $maxAge = 86400): array|false
{
if (empty($initData) || empty($botToken)) {
return false;
}
// 1. Разбираем query string с сохранением исходного кодирования
$rawData = explode('&', $initData);
$params = [];
$receivedHash = '';
foreach ($rawData as $pair) {
if (strpos($pair, '=') === false) {
continue;
}
[$key, $value] = explode('=', $pair, 2);
$decodedKey = urldecode($key);
$decodedValue = urldecode($value);
if ($decodedKey === 'hash') {
$receivedHash = $decodedValue;
} else {
$params[$decodedKey] = $decodedValue;
}
}
if (empty($receivedHash)) {
return false;
}
// 2. Сортируем параметры по алфавиту ключей
ksort($params);
// 3. Формируем data_check_string
$dataCheckArr = [];
foreach ($params as $key => $value) {
$dataCheckArr[] = $key . '=' . $value;
}
$dataCheckString = implode("
", $dataCheckArr);
// 4. Генерируем HMAC-SHA-256 секретный ключ
// Константа 'WebAppData' используется в качестве ключа для хэширования токена бота
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);
// 5. Вычисляем итоговый hash
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);
// 6. Timing-safe сравнение хэшей
if (!hash_equals($calculatedHash, $receivedHash)) {
return false;
}
// 7. Проверка срока жизни auth_date
$authDate = isset($params['auth_date']) ? (int)$params['auth_date'] : 0;
if ($authDate === 0 || (time() - $authDate) > $maxAge) {
return false;
}
// Декодируем поле user, если оно присутствовало
if (isset($params['user'])) {
$params['user'] = json_decode($params['user'], true) ?? $params['user'];
}
return $params;
}
Validating auth_date and Protecting Against Replay Attacks
Even if the check hash matches, a vulnerability remains if the signature can be reused indefinitely. An attacker who intercepts someone else's initData can send it to your server from any device.
Checking auth_date only partially solves this problem by limiting the validity window of the packet (for example, to 86,400 seconds). To prevent the reuse of genuine data within this window (Replay Attack), you need to log unique identifiers on the backend side.
- Store the
query_idor computedhashin Redis / DB with a Time To Live (TTL) equal to the allowedauth_datewindow. - If the received
hashis already present in the store, reject the request with HTTP code403 Forbidden.
Receiving Requests from a Mini App and Working with the Telegram Bot API
Let's consider a practical endpoint that receives initData in an HTTP header, validates it, extracts the user ID, and sends a service notification using the Bot API's sendMessage method via cURL.
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: '';
if (empty($botToken)) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Bot token is not configured']);
exit;
}
// Извлекаем initData из заголовков или POST-тела
$headers = getallheaders();
$initData = $headers['X-Telegram-Init-Data'] ?? $_POST['init_data'] ?? '';
$validData = validateTelegramInitData($initData, $botToken, 86400);
if ($validData === false) {
http_response_code(401);
echo json_encode(['ok' => false, 'error' => 'Invalid or expired initData']);
exit;
}
$user = $validData['user'] ?? null;
if (!is_array($user) || !isset($user['id'])) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'User payload missing']);
exit;
}
$telegramUserId = (int)$user['id'];
// Пример безопасной отправки сообщения пользователю через cURL
$telegramApiUrl = "https://api.telegram.org/bot{$botToken}/sendMessage";
$postFields = [
'chat_id' => $telegramUserId,
'text' => "Успешная авторизация в Mini App! Ваш ID: {$telegramUserId}",
'parse_mode' => 'HTML',
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $telegramApiUrl,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($postFields),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
// В продакшене необходимо записать $curlError в лог
http_response_code(502);
echo json_encode(['ok' => false, 'error' => 'Failed to notify user via Telegram API']);
exit;
}
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || !($responseData['ok'] ?? false)) {
http_response_code(502);
echo json_encode(['ok' => false, 'error' => 'Telegram API returned error']);
exit;
}
// Успешный ответ клиенту
echo json_encode([
'ok' => true,
'user_id' => $telegramUserId,
'first_name' => $user['first_name'] ?? '',
]);
Common Mistakes When Implementing Verification
Even with the correct algorithm, developers make critical mistakes leading either to false positives or vulnerabilities:
- Forgetting the
raw_output = trueargument in HMAC: Callinghash_hmac('sha256', $botToken, 'WebAppData')without the fourth parameter set totruereturns a hex string instead of a raw binary byte array. This breaks the final hash calculation. - Comparing with
===instead ofhash_equals(): The strict equality operator stops comparing on the first mismatched byte, creating an opportunity for a Timing Attack to forge the signature. - Mutating the
userJSON string: Decoding the JSON from theuserparameter before calculatingdata_check_stringand re-assembling it viajson_encode()changes the formatting (spaces, escaped slashes). The signature will no longer match. - Missing HTTP response code check in cURL: When sending requests to the Telegram API, always verify the HTTP response code and
json_last_error()status to handle timeouts and network failures in a timely manner.
If you need end-to-end development of a Telegram Mini App and ticket/lead processing infrastructure, the specialists at BotCreator can help design the architecture and secure your API.