Telegram Mini App on Vue 3 — is a regular SPA inside the WebView client. The SDK provides a theme, viewport height, MainButton button, and the initData string. Below is a practical scheme: connecting the WebApp, safe area / viewport without \"jumps\", the difference between initData and initDataUnsafe, and why HMAC needs to be checked on the backend.
Connecting the SDK and composable
\nThe Telegram script is included in index.html before the Vue bundle. This ensures the always-up-to-date client version, without a separate npm package:
<!-- index.html -->
<head>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>\nThe Composable wraps window.Telegram.WebApp, calls ready() / expand() and returns reactive fields:
// composables/useTelegram.js
import { reactive, readonly } from 'vue'
export function useTelegram() {
const tg = window.Telegram?.WebApp
if (!tg) {
throw new Error('Telegram.WebApp is not available')
}
const state = reactive({
viewportHeight: tg.viewportHeight,
viewportStableHeight: tg.viewportStableHeight,
isExpanded: tg.isExpanded,
colorScheme: tg.colorScheme,
})
tg.ready()
tg.expand()
tg.onEvent('viewportChanged', () => {
state.viewportHeight = tg.viewportHeight
state.viewportStableHeight = tg.viewportStableHeight
state.isExpanded = tg.isExpanded
document.documentElement.style.setProperty(
'--tg-viewport-stable-height',
`${tg.viewportStableHeight}px`
)
})
// первичная установка CSS-переменной
document.documentElement.style.setProperty(
'--tg-viewport-stable-height',
`${tg.viewportStableHeight}px`
)
return {
tg,
state: readonly(state),
initData: tg.initData,
initDataUnsafe: tg.initDataUnsafe,
}
}\nIn the application root:
\n// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')\n// App.vue (script setup)
import { onMounted } from 'vue'
import { useTelegram } from './composables/useTelegram'
const { tg, state } = useTelegram()
onMounted(() => {
// тема из Telegram уже пробрасывается в CSS-переменные --tg-theme-*
document.body.style.backgroundColor = tg.backgroundColor || ''
})\n\nViewport and safe area
\nThe mobile WebView changes its height when the keyboard appears and panels are collapsed. 100vh here lies. Refer to viewportStableHeight and Telegram CSS variables.
/* styles.css */
html, body, #app {
margin: 0;
min-height: var(--tg-viewport-stable-height, 100vh);
}
.app-shell {
min-height: var(--tg-viewport-stable-height, 100vh);
padding-top: var(--tg-safe-area-inset-top, 0px);
padding-bottom: calc(
var(--tg-safe-area-inset-bottom, 0px) + var(--tg-content-safe-area-inset-bottom, 0px)
);
box-sizing: border-box;
}
.checkout-bar {
position: sticky;
bottom: 0;
/* не перекрывать жестом «домой» и MainButton */
padding-bottom: max(12px, var(--tg-safe-area-inset-bottom, 0px));
}\nThe viewportChanged event updates the height. For a fixed \"Submit\" button, the MainButton SDK or a sticky block taking safe area into account is better — otherwise content will go under the system bar.
\n\n\nIf you need fullscreen mode, call
\ntg.expand()immediately afterready(). On clients withoutexpand()the app opens at compact height and \"jumps\" on the first scroll.
initData vs initDataUnsafe
\nTelegram.WebApp.initData is a signed string (query string): user parameters plus the hash field. It must also be sent to the backend.
Telegram.WebApp.initDataUnsafe is already a parsed JavaScript object. Useful for UI (\"Hello, {{first_name}}\"), but it is not proof that the request came from Telegram. Anyone can substitute someone else's user.id in their frontend and hit your API.
- \n
- UI, local hints — can read
initDataUnsafe. \n - Order, balance, personal data — only after verifying HMAC against raw
initDataon the server. \n
Request from the frontend
\nPass the string whole, without manual \"assembly\" of parameters:
\n// api.js
export async function apiPost(path, body) {
const tg = window.Telegram.WebApp
const res = await fetch(path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Telegram-Init-Data': tg.initData,
},
body: JSON.stringify(body),
})
if (!res.ok) {
throw new Error('API ' + res.status)
}
return res.json()
}\n\nHMAC verification on PHP
\nAlgorithm from Telegram documentation: secret = HMAC-SHA256 of the bot token with key WebAppData; the verification string is a series of key=value pairs, sorted by key, without hash, joined by \\n.
<?php
/**
* @return array{ok:bool, user:?array, auth_date:?int, error?:string}
*/
function validateTelegramWebAppInitData(string $initData, string $botToken, int $maxAgeSec = 86400): array
{
parse_str($initData, $data);
if (!isset($data['hash']) || !is_string($data['hash'])) {
return ['ok' => false, 'user' => null, 'auth_date' => null, 'error' => 'no hash'];
}
$hash = $data['hash'];
unset($data['hash']);
ksort($data);
$pairs = [];
foreach ($data as $k => $v) {
$pairs[] = $k . '=' . $v;
}
$dataCheckString = implode("\n", $pairs);
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);
$calculated = hash_hmac('sha256', $dataCheckString, $secretKey);
if (!hash_equals($calculated, $hash)) {
return ['ok' => false, 'user' => null, 'auth_date' => null, 'error' => 'bad hash'];
}
$authDate = isset($data['auth_date']) ? (int) $data['auth_date'] : 0;
if ($authDate <= 0 || (time() - $authDate) > $maxAgeSec) {
return ['ok' => false, 'user' => null, 'auth_date' => $authDate, 'error' => 'expired'];
}
$user = null;
if (!empty($data['user'])) {
$user = json_decode($data['user'], true);
}
return ['ok' => true, 'user' => is_array($user) ? $user : null, 'auth_date' => $authDate];
}
// пример в контроллере
$initData = $_SERVER['HTTP_X_TELEGRAM_INIT_DATA'] ?? '';
$result = validateTelegramWebAppInitData($initData, getenv('TELEGRAM_BOT_TOKEN'));
if (!$result['ok']) {
http_response_code(401);
echo json_encode(['error' => $result['error']]);
exit;
}
$userId = (int) ($result['user']['id'] ?? 0);\nWithout this check any client can forge an order \"on behalf of\" another user. The auth_date expiry limits replay of old strings.
sendData — a separate trap
\ntg.sendData() works only if the Mini App is opened with the keyboard button (reply keyboard). From the Menu Button, inline buttons, or direct links the method is useless. For orders and payments send data to your own API + the bot responds via Bot API.
Brief checklist
\n- \n
- SDK in
index.html, composable withready/expand/viewportChanged. \n - Height and margins — via viewport/safe-area variables, not via \"raw\"
100vh. \n - UI may look at
initDataUnsafe; the server trusts only the HMAC frominitData. \n - Bot token only on the backend; there should be none on the frontend. \n
Need help connecting the Vue Mini App + PHP validation for your bot — we can sketch a scheme on botservice.biz.
"}