Разработка · 8 мин чтения

Telegram Web App: интерфейс внутри мессенджера и передача данных

Telegram Web App превращает бота в полноценный интерфейс: вместо переписки командами пользователь видит кнопки, каталог товаров, календарь записи или форму заявки прямо во всплывающем окне поверх чата. Я собираю такие мини-приложения на связке HTML, CSS и JS для фронтенда и PHP или aiogram-бота для обработки данных, в зависимости от того, что уже есть у клиента и нужна ли отдельная логика на сервере. Дальше разбираю, как подключить Web App к боту, как передать данные между интерфейсом и сервером и почему без проверки подписи initData доверять этим данным нельзя.

Как устроен Telegram Web App и как подключить его к боту

Web App это обычная HTTPS-страница, которая открывается не в отдельном браузере, а внутри клиента Telegram через встроенный веб-вью. Разница с открытием ссылки в моментальном просмотре или во внутреннем браузере Telegram в том, что на страницу подключается скрипт telegram-web-app.js, и через него в JavaScript появляется объект window.Telegram.WebApp: данные о пользователе, тема оформления, кнопки интерфейса, доступ к вибрации и локальному хранилищу. Обычная ссылка этого моста не даёт, там страница живёт сама по себе.

Запустить мини-приложение можно семью способами, в документации они перечислены дословно: «Telegram currently supports seven different ways of launching Mini Apps: the main Mini App from a profile button, from a keyboard button, from an inline button, from the bot menu button, via inline mode, from a direct link», и седьмой это запуск из меню вложений (attachment menu). То есть кроме кнопки в обычной клавиатуре бота, инлайн-кнопки в сообщении, кнопки меню чата и прямой ссылки вида t.me/имя_бота/имя_приложения есть главное мини-приложение из кнопки в профиле бота, запуск из результатов инлайн-режима и запуск из меню вложений. Кнопку меню и параметры прямой ссылки настраивают через BotFather: команда /mybots, затем выбор бота, Bot Settings, Menu Button, Configure Menu Button, куда вставляется адрес страницы и текст кнопки. Инлайн- и обычные кнопки с типом web_app создаются прямо в коде бота.

Вот как добавить инлайн-кнопку с мини-приложением в aiogram 3:

from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
from aiogram.types import WebAppInfo, InlineKeyboardMarkup, InlineKeyboardButton

dp = Dispatcher()

@dp.message(Command("start"))
async def start_handler(message: types.Message):
    keyboard = InlineKeyboardMarkup(inline_keyboard=[[
        InlineKeyboardButton(
            text="Открыть каталог",
            web_app=WebAppInfo(url="https://example.com/webapp/")
        )
    ]])
    await message.answer("Выбирайте товары в мини-приложении:", reply_markup=keyboard)

Выбор способа запуска не формальность, от него зависит, как потом забирать данные из мини-приложения. Ниже таблица с разницей.

Способ запуска Доступен tg.sendData() Как получать данные на сервере
Кнопка в обычной клавиатуре (reply keyboard, web_app) Да Событие с web_app_data в обработчике сообщений бота
Инлайн-кнопка в сообщении (web_app) Нет fetch() из мини-приложения на свой бэкенд + проверка initData
Кнопка меню бота (Menu Button) Нет fetch() из мини-приложения на свой бэкенд + проверка initData
Прямая ссылка (Direct Link Mini App) Нет fetch() из мини-приложения на свой бэкенд + проверка initData

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Передача данных: initData, initDataUnsafe и sendData

После загрузки страницы первым делом вызывают tg.ready() и, если нужно на весь экран, tg.expand(). Дальше у объекта WebApp есть два поля с данными о сеансе: initData, строка с подписанными параметрами, и initDataUnsafe, тот же набор, но уже разобранный в обычный объект с полями user, query_id, auth_date, start_param и hash. Unsafe в названии не для красоты: эти данные пришли из JS в браузере пользователя, их можно подделать, поэтому доверять им напрямую на сервере нельзя, только проверенной подписи из initData.

Если мини-приложение открыто кнопкой из обычной клавиатуры, для отправки данных в бота есть метод sendData. Он принимает строку до 4096 байт, отправляет её как обновление с типом web_app_data в чат с ботом и сразу закрывает окно приложения:

const tg = window.Telegram.WebApp;
tg.ready();
tg.expand();

document.getElementById('checkout').addEventListener('click', () => {
  const payload = JSON.stringify({ items: cart, total: cartTotal });
  tg.sendData(payload);
});

На стороне бота это ловится отдельным фильтром:

from aiogram import F

@dp.message(F.web_app_data)
async def webapp_data_handler(message: types.Message):
    payload = message.web_app_data.data
    await message.answer(f"Заказ принят: {payload}")

Для инлайн-кнопки, кнопки меню и прямой ссылки sendData не сработает, приложение при таком запуске не закрывается автоматически и работает как обычная страница с собственной логикой. В этих случаях данные отправляют обычным запросом на свой бэкенд вместе с initData, чтобы сервер мог проверить подлинность отправителя:

const tg = window.Telegram.WebApp;
tg.ready();

fetch('https://example.com/api/webapp-order', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    initData: tg.initData,
    items: cart
  })
});

Этот же путь удобнее для больших заказов: в sendData помещаются только идентификаторы и количество, а не полные названия и описания товаров, иначе легко упереться в лимит.

Проверка подписи initData на сервере

InitData подписан токеном бота, и это единственный способ убедиться, что запрос действительно пришёл из Telegram, а не был собран вручную. Алгоритм такой: из строки убирают поле hash, оставшиеся пары сортируют по ключу и склеивают в строку через перенос строки, затем считают HMAC-SHA-256 сначала от токена бота с ключом «WebAppData», чтобы получить секретный ключ, а потом от собранной строки с этим секретным ключом. Результат сравнивают с присланным hash.

На обычном хостинге, где уже крутится сайт на Tilda или WordPress, эту проверку можно оформить отдельным PHP-обработчиком, без выделенного сервера:

<?php
$botToken = getenv('BOT_TOKEN');
$initData = $_POST['initData'];

parse_str($initData, $data);
$hash = $data['hash'] ?? '';
unset($data['hash']);
ksort($data);

$pairs = [];
foreach ($data as $key => $value) {
    $pairs[] = $key . '=' . $value;
}
$checkString = implode("\n", $pairs);

$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);
$computedHash = hash_hmac('sha256', $checkString, $secretKey);

if (!hash_equals($computedHash, $hash)) {
    http_response_code(403);
    exit('Invalid signature');
}

$user = json_decode($data['user'], true);

Токен бота в этой схеме нужен только на сервере. Класть его в JS-код мини-приложения нельзя ни в каком виде, иначе подписывать поддельные запросы сможет кто угодно, кто откроет исходники страницы.

Тема, кнопки и другие возможности WebApp SDK

Объект WebApp даёт не только доступ к данным пользователя, но и набор готовых элементов интерфейса. Через themeParams и событие themeChanged страница может подстроиться под тему аккаунта: цвет фона, текста, кнопок и ссылок передаются в CSS-переменных вида var( - tg-theme-button-color), а поле colorScheme подсказывает, светлая сейчас тема или тёмная.

MainButton, большая кнопка внизу экрана, и BackButton, кнопка назад в шапке, управляются вызовами вроде tg.MainButton.setText(), .show() и .onClick(), и их удобнее использовать вместо своих кнопок в разметке, потому что они и выглядят нативно, и не съедают место на маленьком экране. HapticFeedback добавляет отклик при нажатиях, а CloudStorage подходит для хранения небольших пользовательских настроек прямо в аккаунте Telegram, без своей базы под них.

Где хостить фронтенд и на чём собирать бэкенд

Страница мини-приложения не требует отдельного сервера: если сайт клиента уже работает на Tilda или WordPress, у него и так есть HTTPS, и страницу приложения можно разместить прямо там же, добавив нужный JS. Отдельный сервер под саму статику не нужен.

Другой вопрос, где обрабатывать данные. Если задача ограничивается тем, чтобы принять initData, проверить подпись и записать заказ в базу или переслать менеджеру, этого достаточно на обычном хостинге: PHP-обработчик и таблица в MySQL справляются без проблем, а блокировку слота на время оплаты решает транзакция с уникальным индексом на связку мастер, дата и время. Отдельный сервер под это поднимать избыточно.

Если же боту нужна полноценная логика: обрабатывать входящие сообщения, слать напоминания по расписанию, вести диалог, для этого обычно пишут отдельного бота, например на aiogram, а такому боту нужен постоянно работающий процесс. Это уже отдельный сервер, который придётся настраивать, обновлять, продлевать сертификат и оплачивать помесячно сверх хостинга сайта. Если такой процесс не хочется поддерживать самому, а нужен разбор заявок из мини-приложения, ответы клиентам и пересылка в CRM, дешевле по времени заказать разработку Telegram-бота, чем разбираться с вебхуками и сервером с нуля. Отдельно стоит вариант с n8n: если задача это не диалоговый бот, а цепочка «данные из мини-приложения, запись в таблицу, уведомление в чат», сценарий в n8n закрывает это без написания отдельного бота под каждый чих.

Практические сценарии: магазин, запись и заявки

Мини-магазин в Telegram обычно выглядит так: каталог рендерится в WebApp, корзина хранится в localStorage самой страницы, а по кнопке «Оформить» состав заказа уходит в бот через sendData или fetch с id товаров и количеством. Бот или бэкенд создаёт заказ, запрашивает стоимость доставки по зонам СДЭК и присылает пользователю ссылку на оплату через эквайринг Т‑Банка. Полные названия и описания товаров в sendData не гоняют, только идентификаторы, чтобы не упереться в лимит по объёму.

Запись на услугу через мини-приложение удобна тем, что initDataUnsafe.user уже содержит имя и username, и отдельная форма регистрации не нужна, пользователь сразу видит календарь свободных слотов. Бронь слота на сервере оформляют транзакцией с уникальным индексом по мастеру, дате и времени, это защищает от двойной записи, если два человека жмут «Записаться» одновременно, и для этого хватает обычной MySQL на хостинге сайта.

Форма заявки в мини-приложении заменяет лендинг с полями «имя» и «телефон»: имя и username Telegram подставляются автоматически, пользователю остаётся указать разве что телефон и комментарий, а данные уходят в CRM через тот же PHP-обработчик, что проверяет initData.

Условия и возможности Telegram меняются, поэтому перед запуском я уточняю их у самого сервиса.

Частые вопросы

Чем Telegram Web App отличается от обычной ссылки, открытой во внутреннем браузере Telegram

Обычная ссылка открывается как отдельная страница без связи с ботом: у неё нет доступа к данным пользователя, темам оформления, MainButton, BackButton и другим элементам SDK. Web App подключает скрипт telegram-web-app.js, и через объект window.Telegram.WebApp страница получает initData, тему аккаунта и управление кнопками интерфейса.

Можно ли открыть мини-приложение без своего бота, просто по ссылке

Да, для этого в BotFather регистрируют Direct Link Mini App с коротким именем, и приложение открывается по адресу вида t.me/имя_бота/имя_приложения. Бот при этом всё равно нужен как владелец приложения, но диалоговую логику под такой запуск можно не писать, если данные обрабатываются напрямую через fetch на свой бэкенд.

Какой объём данных можно передать через sendData

Метод ограничен 4096 байтами и работает только для мини-приложений, запущенных кнопкой из обычной клавиатуры. Для более крупных заказов или для запуска через инлайн-кнопку, меню бота и прямую ссылку данные передают запросом с бэкенда, а не через sendData, обычно вместе с initData для проверки подписи.

Нужен ли отдельный SSL-сертификат для запуска Web App

HTTPS для адреса мини-приложения обязателен, но отдельно покупать сертификат чаще всего не нужно: если страница размещена на уже работающем сайте на Tilda или WordPress, у него HTTPS уже подключен вместе с хостингом.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно