How to Build a Telegram Mini App for Business in 2026
Telegram Mini Apps (TMAs) are web applications embedded inside Telegram. The user taps a button in a chat, a full-screen web app opens — no redirect to a browser, no app store download, no account creation. It runs inside Telegram, with access to the user's Telegram identity and payment method.
For businesses targeting CIS markets (Russia, Kazakhstan, Kyrgyzstan, Ukraine, Belarus), this matters enormously. Telegram penetration in these markets is 70-90%+. A Mini App reaches your customers where they already spend hours per day.
What You Can Build
The use cases that work particularly well:
E-commerce storefront. Full product catalog, cart, checkout — all inside Telegram. Telegram Payments handles the transaction. The user never leaves the app.
Appointment booking. Calendar, slot selection, confirmation — integrated with your backend. Reminder sent via the bot automatically.
Loyalty programs. Show points balance, available rewards, transaction history. Users check it frequently because it's in their daily-use app.
Food ordering for restaurants. Menu browsing, customization, payment, order status — the complete delivery flow.
Support portal. Create tickets, view status, attach files, receive updates — all in a familiar interface.
Architecture Overview
Telegram Client
↓
Mini App (React/Vue/Vanilla JS — hosted on your server)
↓
Telegram Bot API (validates InitData, handles payments)
↓
Your Backend API (Node.js / Python / Go)
↓
Database / Third-party services
The Mini App is a regular web app served over HTTPS. Telegram injects a window.Telegram.WebApp object that gives you access to the user's data, theme, and native UI components.
Step 1: Set Up the Bot
# Talk to @BotFather in Telegram
/newbot
# Choose a name and username
# Get your BOT_TOKEN
/newapp
# Link your Mini App to the bot
# Set the Web App URL (must be HTTPS)
For development, use ngrok or localtunnel to expose localhost over HTTPS.
Step 2: Frontend Setup
Install the Telegram Web App SDK:
npm create vite@latest my-mini-app -- --template react-ts
cd my-mini-app
npm install @twa-dev/sdk
npm install @twa-dev/types # TypeScript types
Basic app structure:
// src/App.tsx
import { useEffect, useState } from 'react';
import WebApp from '@twa-dev/sdk';
interface TelegramUser {
id: number;
first_name: string;
last_name?: string;
username?: string;
language_code?: string;
}
export default function App() {
const [user, setUser] = useState<TelegramUser | null>(null);
useEffect(() => {
// Initialize the Mini App
WebApp.ready();
WebApp.expand(); // Full-screen mode
// Get user data
if (WebApp.initDataUnsafe.user) {
setUser(WebApp.initDataUnsafe.user as TelegramUser);
}
// Apply Telegram theme to your app
document.documentElement.style.setProperty(
'--tg-theme-bg-color',
WebApp.themeParams.bg_color ?? '#ffffff'
);
document.documentElement.style.setProperty(
'--tg-theme-text-color',
WebApp.themeParams.text_color ?? '#000000'
);
document.documentElement.style.setProperty(
'--tg-theme-button-color',
WebApp.themeParams.button_color ?? '#2481cc'
);
}, []);
return (
<div className="app">
<h1>Hello, {user?.first_name ?? 'User'}!</h1>
</div>
);
}
Step 3: Validate InitData on the Backend
This is security-critical. Anyone can craft a fake initData string. You must validate it on your server before trusting user data.
// backend/src/middleware/telegram-auth.ts
import crypto from 'crypto';
export function validateTelegramInitData(initData: string, botToken: string): boolean {
const params = new URLSearchParams(initData);
const hash = params.get('hash');
if (!hash) return false;
params.delete('hash');
// Sort parameters alphabetically
const dataCheckString = Array.from(params.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join('\n');
// HMAC-SHA256 with "WebAppData" as key derivation
const secretKey = crypto
.createHmac('sha256', 'WebAppData')
.update(botToken)
.digest();
const expectedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex');
return hash === expectedHash;
}
export function parseTelegramUser(initData: string) {
const params = new URLSearchParams(initData);
const userParam = params.get('user');
if (!userParam) return null;
return JSON.parse(decodeURIComponent(userParam));
}
// Express middleware
export function telegramAuthMiddleware(req: any, res: any, next: any) {
const initData = req.headers['x-telegram-init-data'] as string;
if (!initData) {
return res.status(401).json({ error: 'No init data' });
}
if (!validateTelegramInitData(initData, process.env.BOT_TOKEN!)) {
return res.status(401).json({ error: 'Invalid init data' });
}
req.telegramUser = parseTelegramUser(initData);
next();
}
On the frontend, send initData with every request:
// src/api/client.ts
import WebApp from '@twa-dev/sdk';
export async function apiFetch(path: string, options: RequestInit = {}) {
const response = await fetch(`${import.meta.env.VITE_API_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Telegram-Init-Data': WebApp.initData,
...options.headers,
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
Step 4: Native UI Components
The Telegram Web App SDK gives you native UI elements that look and behave like Telegram's own UI:
import WebApp from '@twa-dev/sdk';
// Main button (fixed at the bottom, Telegram-styled)
WebApp.MainButton.setText('Place Order — $24.99');
WebApp.MainButton.show();
WebApp.MainButton.onClick(() => {
handleCheckout();
});
// Show/hide main button based on cart state
function updateMainButton(cartTotal: number) {
if (cartTotal > 0) {
WebApp.MainButton.setText(`Checkout — $${cartTotal.toFixed(2)}`);
WebApp.MainButton.show();
} else {
WebApp.MainButton.hide();
}
}
// Back button
WebApp.BackButton.show();
WebApp.BackButton.onClick(() => {
navigate(-1);
WebApp.BackButton.hide();
});
// Confirmation popup (native Telegram dialog)
WebApp.showConfirm(
'Remove item from cart?',
(confirmed) => {
if (confirmed) removeFromCart(itemId);
}
);
// Haptic feedback
WebApp.HapticFeedback.impactOccurred('medium'); // on button press
WebApp.HapticFeedback.notificationOccurred('success'); // on order success
Step 5: Telegram Payments
Telegram Payments bypass app store fees (no 30% cut). You connect a payment provider (Stripe, YooKassa, Payme, etc.) through BotFather.
// backend: create an invoice link
const bot = new TelegramBot(process.env.BOT_TOKEN!);
async function createInvoice(userId: number, order: Order) {
const invoice = await bot.createInvoiceLink(
`Order #${order.id}`, // title
`${order.items.length} items`, // description
JSON.stringify({ orderId: order.id }), // payload (returned on success)
'USD', // currency
[{ label: 'Total', amount: order.totalCents }], // prices in cents
{
photo_url: 'https://yoursite.com/order-preview.jpg',
need_name: false,
need_email: true,
need_phone_number: true,
}
);
return invoice; // Send this URL to the Mini App
}
// frontend: open the payment dialog
const invoiceUrl = await apiFetch('/api/create-invoice', {
method: 'POST',
body: JSON.stringify({ orderId }),
});
WebApp.openInvoice(invoiceUrl, (status) => {
if (status === 'paid') {
WebApp.showAlert('Payment successful! Your order is confirmed.');
navigate('/order-success');
} else if (status === 'cancelled') {
WebApp.showAlert('Payment cancelled.');
}
});
On the backend, handle the successful payment webhook:
// backend: handle pre_checkout_query and successful_payment
bot.on('pre_checkout_query', (query) => {
// Validate the order is still available
bot.answerPreCheckoutQuery(query.id, true);
});
bot.on('successful_payment', async (msg) => {
const payload = JSON.parse(msg.successful_payment!.invoice_payload);
await markOrderPaid(payload.orderId);
await bot.sendMessage(msg.chat.id, '✅ Order confirmed! We\'ll notify you when it ships.');
});
Step 6: Notifications Back to Users
After the Mini App session ends, you can still reach users via the bot:
async function sendOrderUpdate(telegramUserId: number, order: Order) {
await bot.sendMessage(telegramUserId,
`📦 Order #${order.id} update\n\n` +
`Status: ${order.status}\n` +
`Estimated delivery: ${order.estimatedDelivery}`,
{
reply_markup: {
inline_keyboard: [[
{
text: '🔍 Track Order',
web_app: { url: `https://yourapp.com/orders/${order.id}` }
}
]]
}
}
);
}
Step 7: Deploy
Mini Apps require HTTPS. Production deployment options:
# nginx config for your Mini App
server {
listen 443 ssl;
server_name miniapp.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/miniapp.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/miniapp.yourdomain.com/privkey.pem;
# Frontend (Vite build output)
location / {
root /var/www/miniapp/dist;
try_files $uri $uri/ /index.html;
# Required CORS headers for Telegram
add_header Access-Control-Allow-Origin "https://web.telegram.org";
}
# Backend API
location /api {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Testing
Telegram provides a test environment separate from production:
- In BotFather:
/starton @BotFather → use test server bots - On mobile: Settings → long press on title → Switch to Test Mode
- Desktop:
tdesktop://testmodeor launch with--testflag
For local development, set VITE_API_URL=http://localhost:3001 and use ngrok for the Mini App URL.
Launch Checklist
-
WebApp.ready()called immediately on load (stops the loading indicator) -
WebApp.expand()called if you want full-screen - All API calls validate
initDataserver-side -
WebApp.close()called when the user completes the main action - Handle
viewportChangedevent for safe area adjustments - Test on both iOS and Android Telegram clients
- Test with slow 3G network (Telegram users are global)
- Privacy policy URL set in BotFather
Aunimeda builds Telegram Mini Apps for businesses across CIS markets — from product catalogs and booking systems to full e-commerce flows with Telegram Payments.
Contact us to scope your Mini App project. See also: Telegram Bot Development, Business Automation, Web Development