A Telegram Mini App gives users a full interface without leaving the bot: browsing a catalog, filling a cart, booking a slot. Technically it is an ordinary web page that Telegram opens in its built-in browser and enriches with a JavaScript object for user data, theme colors and buttons. So no special "Telegram server" is needed; you host it like any website. Our example is an ordering app for a bakery in Samarkand.
What a Mini App is made of
There is a frontend (HTML, CSS and JavaScript, plain or built with React or Vue) and a backend API that accepts orders and verifies users. Keeping both on one subdomain, with the frontend at app.misol.uz and the API under /api/, avoids CORS headaches. A static frontend fits almost any hosting; a PHP backend runs fine on shared hosting, while Node.js or Python backends are easier on a VPS.
Hosting and HTTPS
BotFather accepts only HTTPS URLs. On sayt.uz hosting you add a subdomain in the cabinet and attach a free Let's Encrypt certificate that renews automatically, which matters because an expired certificate turns the Mini App into a blank screen. Upload the built dist files and, for client-side routing on Apache, add:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/api/
RewriteRule ^ index.html [L]
The frontend
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<script>
const tg = window.Telegram.WebApp;
tg.ready();
tg.expand();
tg.MainButton.setText("Place order").show();
tg.MainButton.onClick(async () => {
await fetch("/api/order.php", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Init-Data": tg.initData },
body: JSON.stringify({ items: cart })
});
tg.close();
});
</script>
ready() tells Telegram the app has loaded, expand() opens it to full height, and MainButton is the native-looking button at the bottom of the screen. Use tg.themeParams for colors so the app follows the user's light or dark theme and does not look out of place inside Telegram.
Verifying initData on the server
Never trust initDataUnsafe; anything in the browser can be forged. Send the raw initData string to the server and check its signature. Per Telegram's documentation, the secret key is HMAC-SHA256 of the bot token with the key WebAppData, and that key signs the sorted field string.
function tg_check_init_data(string $initData, string $botToken): ?array {
parse_str($initData, $data);
if (empty($data['hash'])) return null;
$hash = $data['hash'];
unset($data['hash']);
ksort($data);
$pairs = [];
foreach ($data as $k => $v) $pairs[] = $k . '=' . $v;
$secret = hash_hmac('sha256', $botToken, 'WebAppData', true);
$calc = hash_hmac('sha256', implode("\n", $pairs), $secret);
if (!hash_equals($calc, $hash)) return null;
if (time() - (int)($data['auth_date'] ?? 0) > 86400) return null;
return $data;
}
The user field is JSON you can decode with json_decode, and the auth_date check blocks replayed old requests. Keep the bot token on the server only, outside the web root.
Connecting it in BotFather
- Menu button: set the bot's menu button to
https://app.misol.uz. - Direct link: create an app with
/newappand share its link. - Keyboard button: send a
web_appbutton from the bot.
Telegram's browser may cache files, so make sure built assets have hashed names, which Vite does by default. The official docs explain how to enable web inspector tools in Telegram Desktop and on Android for debugging.