Обновлено: сентябрь 2026
Как сделать Telegram Mini App
Telegram Mini App — это обычная HTTPS-страница, которую мессенджер открывает в системном WebView. Чтобы она стала мини-приложением, нужны три вещи: страница на своём домене, один тег <script> и включённый Mini App в @BotFather. Ниже — рабочий минимум, проверка подписи и четыре места, где WebView ведёт себя не как браузер.
Что это такое технически
Документация Telegram описывает Mini App как интерфейс на JavaScript, который запускается внутри мессенджера и способен полностью заменить любой сайт. Сборки под платформу, публикации в сторе и ревью здесь нет: вы деплоите сайт, а Telegram открывает его в WebView.
Что это именно WebView, видно по разделу отладки: на Android документация предлагает включить Enable WebView Debug, на iOS — Allow Web View Inspection, на Desktop — Inspect по правой кнопке внутри самого окна приложения. Отсюда же следуют все дальнейшие ограничения.
Минимум, который запускается
Скрипт подключается в <head> до всех остальных, после чего появляется объект window.Telegram.WebApp. Номер после вопросительного знака — версия файла, указанная в документации на август 2026:
<!DOCTYPE html>
<html>
<head>
<script src="https://telegram.org/js/telegram-web-app.js?63"></script>
</head>
<body>
<p id="hello"></p>
<script>
const tg = window.Telegram.WebApp;
tg.ready();
tg.expand();
document.getElementById('hello').textContent =
'Привет, ' + (tg.initDataUnsafe.user?.first_name ?? 'гость');
</script>
</body>
</html> ready() сообщает Telegram, что интерфейс готов к показу, и убирает загрузочный плейсхолдер. Документация советует вызывать его как можно раньше, как только загрузились основные элементы: без вызова плейсхолдер снимется только после полной загрузки страницы. expand() разворачивает приложение на максимально доступную высоту.
Регистрация в @BotFather
- Отправьте
/newbot— BotFather спросит имя и username и выдаст токен. Username бота — это 5–32 символа, латиница, цифры и подчёркивание, обязательно с окончаниемbot. /mybots→ выберите бота → Bot Settings → Configure Mini App → Enable Mini App. Здесь же задаётся адрес приложения.- Адрес обязан быть по HTTPS: поле
urlобъекта WebAppInfo в Bot API так и описано — «An HTTPS URL of a Web App». Самоподписанный сертификат не подойдёт. - Кнопка под полем ввода настраивается командой
/setmenubutton(или Bot Settings → Menu Button); нужны текст кнопки и URL. Программный аналог — методsetChatMenuButton.
У одного бота может быть несколько Mini App: они различаются коротким именем, и это же имя стоит в прямой ссылке https://t.me/botusername/appname. Главное приложение бота открывается ссылкой без короткого имени — https://t.me/botusername?startapp.
Как передать параметр внутрь приложения
Это первое место, где легко потерять полдня. Значение startapp из прямой ссылки приезжает в приложение двумя путями сразу: в поле start_param объекта initData и в GET-параметре tgWebAppStartParam. У меню вложений параметр называется иначе — startattach.
Ловушка. Способов запуска у Mini App семь, и start_param есть не во всех. При запуске с клавиатурной кнопки и через inline-режим объект WebAppInitData пуст целиком: там нет ни user, ни query_id, ни параметра запуска. У inline-кнопки и кнопки меню данные пользователя есть, но передача start_param в их разделах документации не описана.
Практический вывод: если приложение открывается inline-кнопкой web_app, параметр надо везти в самом URL кнопки и читать его из location.search, а не ждать в initData.
У deep-link'а самого бота (t.me/botusername?start=…) ограничение жёсткое и задокументированное: 64 символа, алфавит A-Z, a-z, 0-9, _ и -. Для бинарных данных документация советует base64url. Для startapp ни длина, ни алфавит нигде не заданы, так что переносить на него «те же 64» — недокументированное допущение, а не факт.
Как проверить, что пользователь настоящий
Объект initDataUnsafe называется так не для красоты: это распакованные данные без проверки, и опираться на них при принятии решений нельзя. Доверять можно только строке initData, и только после проверки подписи на сервере.
Алгоритм из официальной документации: собрать все полученные поля кроме hash, отсортировать по алфавиту, склеить как key=value через перевод строки, посчитать HMAC-SHA-256 и сравнить с hash.
import hashlib, hmac
from urllib.parse import parse_qsl
def check(init_data: str, bot_token: str) -> bool:
pairs = dict(parse_qsl(init_data))
received = pairs.pop("hash", "")
check_string = "\n".join(f"{k}={v}" for k, v in sorted(pairs.items()))
secret = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
calc = hmac.new(secret, check_string.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(calc, received) Обратите внимание на порядок аргументов в строке, где считается secret. В псевдокоде документации написано secret_key = HMAC_SHA256(<bot_token>, "WebAppData"): токен здесь — сообщение, а строка WebAppData — ключ. Перепутать их местами — самая частая ошибка в этом месте, и она даёт стабильно неверный hash при полностью правильном остальном коде.
Поле auth_date — Unix-время открытия формы, а не отправки запроса. Документация советует его проверять, но никакого срока годности не называет: окно свежести выбираете вы. Вместе с проверкой подписи это единственная защита от переигрывания старых данных.
Если проверять данные должна третья сторона, у которой нет токена бота, с Bot API 8.0 (17 ноября 2024) для этого есть отдельное поле signature — Ed25519-подпись в base64url, проверяемая опубликованным Telegram публичным ключом. Строка для проверки там собирается иначе: первой идёт bot_id:WebAppData, а из полей исключаются и hash, и signature.
Четыре места, где WebView не браузер
- Высота.
viewportHeightобновляется слишком редко, и документация прямо не рекомендует привязывать к нему нижние элементы интерфейса. Для этого естьviewportStableHeight— высота в последнем устойчивом состоянии, которая не дёргается во время жестов и анимаций; в CSS доступна какvar(--tg-viewport-stable-height). - Вырезы и системные панели.
safeAreaInsetдаёт отступыtop/bottom/left/rightв пикселях, каждый продублирован CSS-переменной видаvar(--tg-safe-area-inset-top). Отдельно естьcontentSafeAreaInset— отступы уже не от системного интерфейса, а от элементов самого Telegram. - Версия клиента, а не ваша.
WebApp.version— это версия Bot API, поддерживаемая приложением пользователя. Новый метод, доступный в вашем Telegram, у клиента на старой сборке просто не сработает, поэтому фичи проверяются черезisVersionAtLeast(). - Хранилище. Вместо привычного localStorage —
CloudStorage: до 1024 записей на пользователя для каждого бота, ключ 1–128 символов изA-Z,a-z,0-9,_и-, значение до 4096 символов. Кириллица в ключе недопустима, значение длиннее лимита придётся резать на части.
На какой версии всё это
Актуальная версия Bot API на август 2026 — 10.2, опубликована 14 июля 2026. Сам JS-класс WebApp пополняется реже: последняя запись в разделе Recent changes страницы Mini Apps — 3 апреля 2026, Bot API 9.6, добавлен метод requestChat.
Из этого легко сделать неверный вывод, что старый код заведётся как есть. В той же версии 10.2 Telegram запретил вызывать методы Mini App с origin, отличного от домена самого приложения, и включил эту защиту автоматически для всех приложений 20 июля 2026. Запись об этом лежит в общем changelog Bot API, а не в разделе Recent changes страницы Mini Apps, — поэтому привычка «смотрю Recent changes, значит ничего не менялось» именно здесь и промахивается. Если приложение дёргает Telegram.WebApp.* из фрейма или со страницы на другом домене, после этой даты вызовы перестают работать.
Когда приложение писать не нужно
Всё выше описывает, как сделать Mini App вообще. Если задача узкая — принимать записи клиентов, — то большая часть работы окажется не в WebView, а в том, что за ним: расписание, защита от двойной записи на одно окно, напоминания, отмены, предоплата.
Mira — готовое мини-приложение ровно под этот сценарий, для бьюти-мастеров: лента работ, предоплата по СБП, напоминания, клиентская база и аналитика. Если приложение нужно не само по себе, а чтобы клиентки записывались, проще взять готовое.
Вопросы
Нужен ли сервер, чтобы сделать Telegram Mini App?
initData (подпись считается по токену бота, а токен нельзя отдавать в браузер) или что-то хранить.Чем Mini App отличается от бота?
Сколько символов помещается в start-параметр ссылки на бота?
A-Z, a-z, 0-9, _ и -; для бинарных данных документация советует base64url. Для параметра startapp прямых ссылок на Mini App длина нигде не оговорена.