Telegram Mini App: веб-приложение внутри бота

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"),
    )]
])

Есть идея? Реализуем

Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

Написать в Telegram

29.03.2026

Нужна консультация?

Оставьте свои контактные данные, или свяжитесь с нами удобным для вас способом

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Нажимая кнопку «Принять», вы соглашаетесь на сбор cookie. Мы используем их для обеспечения функционирования веб-сайта, аналитики действий и улучшения качества обслуживания. Если Вы не хотите, чтобы эти данные обрабатывались, отключите cookie в настройках браузера или прекратите использовать сайт.
Принять