Integrating web applications directly into the Telegram Mini Apps messenger interface (TMA) opens up huge opportunities for business. However, when developing on a modern frontend stack, such as Vue 3, developers often face non-obvious problems: a jumping viewport when the keyboard opens, incorrect handling of Telegram color schemes, and vulnerabilities during user data validation on the backend.
In this article, we will look at how to properly connect the Telegram WebApp SDK to a Vue 3 application (with Composition API), how to stabilize the viewport, when and how to use the sendData method, and how to implement reliable cryptographic verification of the received data on the PHP server side.
Connecting WebApp SDK in Vue 3 and Managing the Theme
To integrate a Mini App with the native Telegram client, you need to connect the standard script. The most reliable way is to add the CDN script to the index.html file of your Vue project. This ensures that the global window.Telegram.WebApp object is available immediately upon application initialization.
<!-- index.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<title>My Telegram Mini App</title>
</head>
<body>
<div id="app"></div>
</body>
</html>Inside Vue 3, we wrap the work with the SDK in a component or a composable function. It is important to correctly handle the Telegram color scheme, which is passed via the themeParams object. This will allow the application interface to seamlessly mimic the dark or light theme of the user's messenger.
<!-- App.vue -->
<template>
<div class="tma-wrapper" :style="themeVariables">
<header class="tma-header">
<p>Привет, {{ username }}!</p>
</header>
<main class="tma-content">
<slot></slot>
<button @click="sendPayload" class="tma-btn">Отправить заказ</button>
</main>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
const username = ref('Гость');
const themeParams = ref({});
onMounted(() => {
const tg = window.Telegram?.WebApp;
if (tg) {
tg.ready();
tg.expand(); // Разворачиваем приложение на максимум
username.value = tg.initDataUnsafe?.user?.first_name || 'Пользователь';
themeParams.value = tg.themeParams || {};
// Следим за изменениями темы на лету
tg.onEvent('themeChanged', () => {
themeParams.value = tg.themeParams;
});
}
});
const themeVariables = computed(() => {
const colors = themeParams.value;
return {
'--bg-color': colors.bg_color || '#ffffff',
'--text-color': colors.text_color || '#000000',
'--hint-color': colors.hint_color || '#999999',
'--link-color': colors.link_color || '#2481cc',
'--button-color': colors.button_color || '#2481cc',
'--button-text-color': colors.button_text_color || '#ffffff'
};
});
</script>Managing Viewport and Preventing Minimization
One of the main problems when developing TMAs is the behavior of the viewport on mobile devices, especially when focusing on text input fields. When the on-screen keyboard appears, the window size decreases, which can lead to unwanted displacement of elements or attempts to minimize the Mini App with a swipe-down gesture.
To lock the height and prevent accidental closure of the application by the user, you need to use viewport management methods:
Telegram.WebApp.expand()— forces the application window to expand to the maximum available height.Telegram.WebApp.enableClosingConfirmation()— enables closing confirmation. If the user accidentally pulls the screen down, Telegram will ask if they really want to close the application.- Tracking the
viewportChangedevent with theisStateStableparameter allows you to adapt the interface in time (for example, hiding fixed footers when the keyboard opens).
It is recommended to call tg.enableClosingConfirmation() immediately when mounting the main component of the application.
Sending Data to the Bot: sendData vs. HTTP API
To send data from the Mini App back to the chat with the bot, there is the Telegram.WebApp.sendData(data) method. However, it has a critical limitation that beginner developers often forget about:
Important limitation: ThesendDatamethod works only if the Mini App was launched via a regular bot keyboard button (a key of typeKeyboardButtonwith theweb_appparameter). If the application is opened via an Inline button in a message, via the Attachment Menu, or via a direct link, callingsendDatawill not work.
If your application is launched via a Keyboard Button, calling tg.sendData(JSON.stringify(payload)) will instantly close the Mini App and send a text message on behalf of the user to the chat with the bot. The bot will receive this event in the Update update object in the message.web_app_data field.
In real projects (online stores, CRM, запись на услуги), it is much more reliable to use standard HTTP requests (via Axios or Fetch) to your own backend. To authorize the request, the window.Telegram.WebApp.initData string is passed in the headers. The backend verifies its validity and independently sends messages to the user via the Bot API.
Secure Validation of initData in PHP
Never trust data received directly from the frontend (including initDataUnsafe). An attacker can easily intercept traffic or spoof field values in the browser. To verify parameters, you must use the initData string and check the signature (hash) using a secret key created based on your bot's token.
Below is a ready-to-use, secure class in pure PHP for validating initData using the HMAC-SHA-256 cryptographic algorithm and protection against timing attacks (timing-safe comparison).
<?php
declare(strict_types=1);
class TelegramInitDataValidator
{
private string $botToken;
private int $expireSeconds;
public function __construct(string $botToken, int $expireSeconds = 86400)
{
$this.botToken = $botToken;
$this.expireSeconds = $expireSeconds;
}
/**
* Валидирует строку initData и возвращает массив параметров в случае успеха.
*
* @param string $initData Строка запроса из window.Telegram.WebApp.initData
* @return array|null Данные авторизации или null в случае невалидности
*/
public function validate(string $initData): ?array
{
parse_str($initData, $params);
if (!isset($params['hash']) || !isset($params['auth_date'])) {
return null;
}
// 1. Проверяем актуальность данных по времени (защита от повторного использования старых сессий)
if (time() - (int)$params['auth_date'] > $this.expireSeconds) {
return null;
}
$receivedHash = $params['hash'];
unset($params['hash']);
// 2. Сортируем параметры в алфавитном порядке
ksort($params);
// 3. Формируем строку проверки
$dataCheckArr = [];
foreach ($params as $key => $value) {
$dataCheckArr[] = "{$key}={$value}";
}
$dataCheckString = implode("\n", $dataCheckArr);
// 4. Генерируем секретный ключ на основе токена бота
$secretKey = hash_hmac('sha256', $this.botToken, 'WebAppData', true);
// 5. Вычисляем эталонный хэш
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);
// 6. Сравниваем хэши безопасным методом hash_equals
if (!hash_equals($receivedHash, $calculatedHash)) {
return null;
}
// Декодируем вложенный JSON пользователя для удобства
if (isset($params['user'])) {
$params['user'] = json_decode($params['user'], true);
}
return $params;
}
}Example of using the class in your PHP controller:
<?php
require_once 'TelegramInitDataValidator.php';
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: 'YOUR_BOT_TOKEN';
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
if (preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) {
$initData = $matches[1];
} else {
http_response_code(401);
echo json_encode(['error' => 'Missing authorization header']);
exit;
}
$validator = new TelegramInitDataValidator($botToken);
$userData = $validator->validate($initData);
if ($userData === null) {
http_response_code(403);
echo json_encode(['error' => 'Invalid or expired Telegram initData']);
exit;
}
// Данные успешно проверены, можно доверять $userData['user']['id']
header('Content-Type: application/json');
echo json_encode([
'status' => 'success',
'user_id' => $userData['user']['id'] ?? null,
'username' => $userData['user']['username'] ?? null
]);Typical Mistakes When Integrating Vue and Telegram WebApp
When developing Mini Apps, developers most often make the following architectural mistakes:
- Using initDataUnsafe for business logic. Remember: this object is intended solely for rendering the interface in the frontend (for example, showing an avatar or username). Any operations with balance, orders, or access rights must undergo
initDataverification on the backend. - Ignoring the auth_date parameter. Without checking the data generation time, an attacker can intercept a valid
initDatastring once and use it indefinitely to perform unauthorized actions on behalf of the user. Limit the session lifetime (recommended from 1 to 24 hours). - Incorrect alignment for the keyboard. If you use rigidly fixed elements in Vue (
position: fixed), be sure to test their behavior on real iOS and Android mobile devices with the keyboard open.
If you require professional development of complex integrations and Telegram Mini Apps, the BotCreator team will help implement a project of any complexity on the botservice.biz website.