Telegram Mini App with Vue 3: WebApp SDK, Themes, sendData, and initData Validation in PHP

Developing Telegram Mini Apps (TWA) on a modern Vue 3 stack requires a deep understanding of how the client-side WebView interacts with the Telegram client and the server backend. Unlike standard SPAs, mini-apps function inside an isolated container where you need to properly handle viewport changes, adapt to the user's system color scheme, and protect transmitted data from tampering.

In this article, we will walk through building the client side using Vue 3 with the Composition API, managing UI stability, passing data to the bot, and strictly validating session authenticity on the PHP side.

1. Telegram WebApp SDK Initialization and Composition API in Vue 3

You can include the Telegram WebApp JS SDK via a https://telegram.org/js/telegram-web-app.js script tag in index.html or by installing the npm package. To ensure reactivity in Vue 3, it is practical to extract the interaction with window.Telegram.WebApp into a dedicated composable module useTelegram.

Let's create a reactive hook that initializes the WebApp, tracks the screen expansion state (isExpanded), and provides an interface for calling native methods.

import { ref, onMounted, onUnmounted } from 'vue';

export function useTelegram() {
const tg = window.Telegram?.WebApp;
const user = ref(tg?.initDataUnsafe?.user || null);
const isExpanded = ref(tg?.isExpanded || false);
const themeParams = ref(tg?.themeParams || {});

const handleViewportChanged = () => {
isExpanded.value = tg?.isExpanded || false;
};

onMounted(() => {
if (!tg) {
console.warn('Telegram WebApp SDK не найден');
return;
}

tg.ready();
tg.expand();
tg.onEvent('viewportChanged', handleViewportChanged);
});

onUnmounted(() => {
if (tg) {
tg.offEvent('viewportChanged', handleViewportChanged);
}
});

const sendDataToBot = (data) => {
if (!tg) return;
tg.sendData(JSON.stringify(data));
};

return {
tg,
user,
isExpanded,
themeParams,
sendDataToBot
};
}

2. Viewport Management and Preventing Interface Jitter

One of the main challenges when developing Mini Apps for mobile devices is the instability of the viewport height. Opening the keyboard or a user swipe gesture can collapse the application or result in blank spaces.

To prevent accidental closing of the application via a swipe down and to lock the interface, you need to call the expand() method when mounting the main component, as well as apply CSS variables for dynamic screen height.

import { onMounted } from 'vue';
import { useTelegram } from './useTelegram';

export default {
name: 'App',
setup() {
const { tg, themeParams } = useTelegram();

const applyThemeVariables = () => {
if (!themeParams.value) return;
const root = document.documentElement;
const tp = themeParams.value;

if (tp.bg_color) root.style.setProperty('--tg-theme-bg-color', tp.bg_color);
if (tp.text_color) root.style.setProperty('--tg-theme-text-color', tp.text_color);
if (tp.button_color) root.style.setProperty('--tg-theme-button-color', tp.button_color);
if (tp.button_text_color) root.style.setProperty('--tg-theme-button-text-color', tp.button_text_color);
if (tp.secondary_bg_color) root.style.setProperty('--tg-theme-secondary-bg-color', tp.secondary_bg_color);
};

onMounted(() => {
if (tg) {
tg.enableClosingConfirmation();
applyThemeVariables();
}
});

return {};
}
};

3. Sending Data to the Bot Chat via sendData

The Telegram.WebApp.sendData() method allows you to instantly send a string payload to the bot chat and automatically close the Mini App. An important technical constraint: the sendData() method works only if the web app was opened via a standard keyboard button (KeyboardButton of type web_app). If the app was opened from an Inline button or the system menu, sendData() will throw an SDK error.

Here is an example of sending an order structure from a Vue 3 component:

import { ref } from 'vue';
import { useTelegram } from './useTelegram';

export default {
name: 'OrderForm',
setup() {
const { sendDataToBot } = useTelegram();
const serviceId = ref(102);
const comment = ref('');

const submitForm = () => {
const payload = {
action: 'create_order',
lead_id: Date.now().toString(36),
service_id: serviceId.value,
comment: comment.value
};

sendDataToBot(payload);
};

return { serviceId, comment, submitForm };
}
};

4. Validating initData on the Backend (PHP HMAC-SHA256)

The initDataUnsafe object on the client side can easily be spoofed by a user via DevTools. For secure authentication, the backend must receive the raw initData string (passed via the Authorization HTTP header or in the POST request body) and validate it using the bot token.

Below is a working PHP script for checking the cryptographic signature of initData using binary HMAC-SHA256 and the hash_equals function to protect against timing attacks:

<?php

function validateTelegramInitData(string $initData, string $botToken): ?array
{
parse_str($initData, $data);

if (!isset($data['hash'])) {
return null;
}

$checkHash = $data['hash'];
unset($data['hash']);

$dataCheckArray = [];
foreach ($data as $key => $value) {
$dataCheckArray[] = $key . '=' . $value;
}
sort($dataCheckArray);

$dataCheckString = implode("
", $dataCheckArray);

// 1. Создаем секретный ключ из токена бота
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);

// 2. Вычисляем контрольный хэш от строки данных
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

// 3. Timing-safe сравнение хэшей
if (!hash_equals($calculatedHash, $checkHash)) {
return null;
}

// 4. Проверка устаревания данных (например, не старше 24 часов)
if (isset($data['auth_date']) && (time() - (int)$data['auth_date'] > 86400)) {
return null;
}

return $data;
}

// Пример обработки входящего запроса
header('Content-Type: application/json');

$headers = getallheaders();
$initData = $headers['X-Telegram-Init-Data'] ?? $_POST['init_data'] ?? '';
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: '';

if (empty($initData) || empty($botToken)) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Missing required parameters']);
exit;
}

$validated = validateTelegramInitData($initData, $botToken);

if ($validated === null) {
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'Invalid initData signature or data expired']);
exit;
}

$userData = json_decode($validated['user'] ?? '{}', true);

echo json_encode([
'ok' => true,
'user_id' => $userData['id'] ?? null,
'username' => $userData['username'] ?? null
]);

5. Sending Responses via Bot API in PHP when Working via Inline Mini Apps

If the Mini App was launched from an Inline button, calling sendData() will not work. In this case, the frontend sends an AJAX request to your PHP backend, and the backend makes a direct call to the Telegram Bot API sendMessage method via cURL.

<?php

function sendTelegramMessage(int $chatId, string $text): bool
{
$botToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$botToken) {
error_log('TELEGRAM_BOT_TOKEN is not defined');
return false;
}

$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
$payload = [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML'
];

$ch = curl_init($url);
curl_setopt_array($ch, [
'CURLOPT_POST' => true,
'CURLOPT_RETURNTRANSFER' => true,
'CURLOPT_HTTPHEADER' => ['Content-Type: application/json'],
'CURLOPT_POSTFIELDS' => json_encode($payload),
'CURLOPT_TIMEOUT' => 5,
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

if ($curlError || $httpCode !== 200) {
error_log("Telegram API Error: HTTP {$httpCode}, cURL: {$curlError}");
return false;
}

$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || !($result['ok'] ?? false)) {
error_log("Telegram API non-ok response: " . $response);
return false;
}

return true;
}

6. Common Pitfalls and Pre-Deployment Checklist

  • Relying on sendData everywhere: Remember that sendData does not work for Mini Apps opened via direct links (t.me/bot/app) or from an InlineKeyboardMarkup. Always implement a fallback to your REST API.
  • Missing auth_date validation: Always verify the auth_date field from initData on the server, otherwise an old initData string could be reused by third parties (Replay attack).
  • Directly using window.Telegram: During server-side rendering (SSR / Nuxt) or local browser development, the window.Telegram.WebApp object might be undefined. Always check its existence before calling methods.

The BotCreator team helps develop fault-tolerant webhooks and Mini App architecture.

New articles on Telegram

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