Telegram Mini App — полноценное веб-приложение, которое открывается прямо внутри мессенджера. Магазин, форма бронирования, дашборд, игра — всё это можно запустить без перехода в браузер. Разберём Telegram Web App SDK, валидацию данных, оплату через Stars и адаптацию к темам.
Что такое Mini App
Mini App (ранее Web App) — это обычный веб-сайт, который Telegram загружает в встроенном WebView. Бот отправляет кнопку, пользователь нажимает — открывается ваше приложение. Данные пользователя (id, имя, язык) передаются автоматически.
Требования:
- HTTPS (обязательно, даже для разработки)
- Мобильный адаптив — Mini App открывается во весь экран
- Быстрая загрузка — пользователь не будет ждать 5 секунд
Подключение SDK
Telegram Web App SDK подключается через скрипт. Для React/TypeScript удобнее использовать пакет @telegram-apps/sdk-react:
npm install @telegram-apps/sdk-react
Инициализация в React-приложении:
// src/App.tsx
import { useEffect } from 'react';
import {
initMiniApp,
initMainButton,
initBackButton,
initThemeParams,
postEvent,
} from '@telegram-apps/sdk-react';
function App() {
const miniApp = initMiniApp();
const themeParams = initThemeParams();
useEffect(() => {
// Сообщаем Telegram, что приложение готово
miniApp.ready();
// Разворачиваем на весь экран
miniApp.expand();
}, []);
return (
<div style={{
backgroundColor: themeParams.bgColor,
color: themeParams.textColor,
minHeight: '100vh',
}}>
<h1>Mini App</h1>
</div>
);
}
Валидация initData (HMAC)
Когда Mini App открывается, Telegram передаёт в window.Telegram.WebApp.initData строку с данными пользователя, подписанную HMAC-SHA256. Валидацию нужно делать на сервере — клиент может подделать данные.
Серверная валидация на Python
import hashlib
import hmac
import json
from urllib.parse import parse_qs, unquote
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
BOT_TOKEN = "7123456789:AAH..."
def validate_init_data(init_data: str, bot_token: str) -> dict:
"""Валидация initData по алгоритму Telegram."""
parsed = dict(parse_qs(init_data, keep_blank_values=True))
# Значения — списки, берём первый элемент
data = {k: v[0] for k, v in parsed.items()}
received_hash = data.pop("hash", None)
if not received_hash:
raise ValueError("Missing hash")
# Сортируем параметры и формируем строку
data_check_string = "\n".join(
f"{k}={v}" for k, v in sorted(data.items())
)
# HMAC-SHA256
secret_key = hmac.new(
b"WebAppData", bot_token.encode(), hashlib.sha256
).digest()
calculated_hash = hmac.new(
secret_key,
data_check_string.encode(),
hashlib.sha256,
).hexdigest()
if calculated_hash != received_hash:
raise ValueError("Invalid hash")
# Парсим user
if "user" in data:
data["user"] = json.loads(data["user"])
return data
@app.post("/api/auth")
async def auth(authorization: str = Header()):
try:
data = validate_init_data(authorization, BOT_TOKEN)
except ValueError as e:
raise HTTPException(status_code=401, detail=str(e))
user = data.get("user", {})
return {
"user_id": user.get("id"),
"first_name": user.get("first_name"),
"username": user.get("username"),
}
Отправка initData с клиента
// src/api.ts
const API_URL = import.meta.env.VITE_API_URL;
async function apiRequest<T>(endpoint: string, options?: RequestInit): Promise<T> {
const initData = window.Telegram?.WebApp?.initData || '';
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': initData,
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
export async function getProfile() {
return apiRequest<{ user_id: number; first_name: string }>('/api/auth', {
method: 'POST',
});
}
Адаптация к теме Telegram
Telegram передаёт цвета текущей темы пользователя. Используйте их, чтобы Mini App выглядел как часть мессенджера:
:root {
--tg-bg: var(--tg-theme-bg-color, #ffffff);
--tg-text: var(--tg-theme-text-color, #000000);
--tg-hint: var(--tg-theme-hint-color, #999999);
--tg-link: var(--tg-theme-link-color, #2481cc);
--tg-button: var(--tg-theme-button-color, #2481cc);
--tg-button-text: var(--tg-theme-button-text-color, #ffffff);
--tg-secondary-bg: var(--tg-theme-secondary-bg-color, #f0f0f0);
}
body {
background-color: var(--tg-bg);
color: var(--tg-text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 16px;
}
a { color: var(--tg-link); }
.card {
background: var(--tg-secondary-bg);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.btn-primary {
background: var(--tg-button);
color: var(--tg-button-text);
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
cursor: pointer;
width: 100%;
}
.hint {
color: var(--tg-hint);
font-size: 14px;
}
MainButton и BackButton
MainButton — кнопка внизу экрана (как «Оплатить» в магазинах). BackButton — стрелка «Назад» в заголовке.
// src/hooks/useMainButton.ts
import { useEffect } from 'react';
export function useMainButton(text: string, onClick: () => void, visible = true) {
useEffect(() => {
const tg = window.Telegram?.WebApp;
if (!tg) return;
const btn = tg.MainButton;
btn.text = text;
if (visible) {
btn.show();
btn.onClick(onClick);
} else {
btn.hide();
}
return () => {
btn.offClick(onClick);
btn.hide();
};
}, [text, onClick, visible]);
}
// src/hooks/useBackButton.ts
export function useBackButton(onClick: () => void, visible = true) {
useEffect(() => {
const tg = window.Telegram?.WebApp;
if (!tg) return;
const btn = tg.BackButton;
if (visible) {
btn.show();
btn.onClick(onClick);
} else {
btn.hide();
}
return () => {
btn.offClick(onClick);
btn.hide();
};
}, [onClick, visible]);
}
Использование в компоненте:
// src/pages/Cart.tsx
import { useCallback } from 'react';
import { useMainButton } from '../hooks/useMainButton';
import { useBackButton } from '../hooks/useBackButton';
function Cart({ items, onBack, onCheckout }) {
const total = items.reduce((sum, item) => sum + item.price, 0);
const handleCheckout = useCallback(() => {
const tg = window.Telegram?.WebApp;
tg?.MainButton.showProgress();
onCheckout().finally(() => {
tg?.MainButton.hideProgress();
});
}, [onCheckout]);
useMainButton(`Оплатить ${total} ₽`, handleCheckout, items.length > 0);
useBackButton(onBack);
return (
<div>
<h2>Корзина</h2>
{items.map(item => (
<div className="card" key={item.id}>
<strong>{item.name}</strong>
<span>{item.price} ₽</span>
</div>
))}
</div>
);
}
Оплата через Telegram Stars
Stars — встроенная валюта Telegram. Пользователи покупают Stars и платят ими в Mini App. Для продавца — это реальные деньги (можно вывести).
Создание инвойса (бэкенд)
from aiogram import Bot
from aiogram.types import LabeledPrice
bot = Bot(token=BOT_TOKEN)
async def create_stars_invoice(
title: str,
description: str,
amount: int, # количество Stars
payload: str,
) -> str:
"""Создаёт ссылку на инвойс для оплаты Stars."""
link = await bot.create_invoice_link(
title=title,
description=description,
payload=payload,
currency="XTR", # Telegram Stars
prices=[LabeledPrice(label=title, amount=amount)],
)
return link
Оплата на клиенте
async function payWithStars(invoiceUrl: string) {
const tg = window.Telegram?.WebApp;
if (!tg) return;
tg.openInvoice(invoiceUrl, (status) => {
if (status === 'paid') {
// Оплата прошла
alert('Спасибо за покупку!');
} else if (status === 'cancelled') {
// Пользователь отменил
} else if (status === 'failed') {
alert('Ошибка оплаты');
}
});
}
Обработка pre_checkout_query (обязательно)
from aiogram import Router
from aiogram.types import PreCheckoutQuery, Message
router = Router()
@router.pre_checkout_query()
async def on_pre_checkout(query: PreCheckoutQuery):
# Проверяем, что товар ещё доступен
await query.answer(ok=True)
@router.message(lambda m: m.successful_payment is not None)
async def on_successful_payment(message: Message):
payment = message.successful_payment
payload = payment.invoice_payload
# Активируем покупку по payload
await message.answer(
f"Оплата {payment.total_amount} Stars получена!\n"
f"ID транзакции: {payment.telegram_payment_charge_id}"
)
Типизация для TypeScript
// src/types/telegram.d.ts
interface TelegramWebApp {
initData: string;
initDataUnsafe: {
user?: {
id: number;
first_name: string;
last_name?: string;
username?: string;
language_code?: string;
};
auth_date: number;
hash: string;
};
themeParams: Record<string, string>;
MainButton: {
text: string;
show(): void;
hide(): void;
onClick(cb: () => void): void;
offClick(cb: () => void): void;
showProgress(): void;
hideProgress(): void;
};
BackButton: {
show(): void;
hide(): void;
onClick(cb: () => void): void;
offClick(cb: () => void): void;
};
ready(): void;
expand(): void;
close(): void;
openInvoice(url: string, cb: (status: string) => void): void;
}
interface Window {
Telegram?: {
WebApp?: TelegramWebApp;
};
}
Запуск Mini App через бота
Регистрация Mini App в BotFather:
/mybots → выберите бота → Bot Settings → Menu Button → Edit Menu Button URL
# Вставьте URL вашего Mini App: https://your-app.vercel.app
Отправка кнопки из бота:
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text="Открыть приложение",
web_app=WebAppInfo(url="https://your-app.vercel.app"),
)]
])
Есть идея? Реализуем
Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

