· 7 min read
Building a Practical Telegram Mini App: A Checkout Flow from Bot to Web App
Telegram Mini Apps let you move real product flows into a lightweight web UI without leaving the chat. This tutorial walks through a practical implementation: opening a Mini App from a bot, reading Telegram user data, and submitting a simple checkout action back to your backend.
Telegram Mini Apps are most useful when they remove friction from a chat-based workflow. Instead of sending users to a separate website, you can launch a compact web app directly from a bot, collect a small amount of structured input, and send the result back to your server.
In this tutorial, we will build a practical Mini App flow:
- a Telegram bot sends a button that opens a Mini App
- the Mini App reads Telegram-provided init data
- the user selects a product and confirms checkout
- the app submits the payload to your backend
- the backend verifies the Telegram session data
This is not a toy example. The same pattern works for booking, ordering, lead capture, subscription upgrades, and internal admin tools.
What you need before starting
You should already have:
- a Telegram bot created with BotFather
- a publicly reachable HTTPS domain for your Mini App
- a backend server for verifying Telegram data and handling the action
- basic familiarity with HTML, JavaScript, and a backend framework
A Mini App is just a web application, but Telegram injects a runtime context into it. The important part is not the UI framework; it is how you securely receive and verify Telegram’s data.
How the Telegram Mini App flow works
At a high level:
- The user chats with your bot.
- The bot sends a button that opens the Mini App.
- Telegram loads your web app inside the client.
- Your app reads
Telegram.WebApp.initDataandTelegram.WebApp.initDataUnsafe. - The user completes an action.
- Your app sends the action plus the signed init data to your backend.
- Your backend verifies the init data using your bot token.
The critical security rule is this: do not trust initDataUnsafe alone. It is useful for convenience, but your backend must verify the signed payload before accepting anything important.
Step 1: Create the bot button that opens the Mini App
Telegram supports launching a web app from a bot message using a keyboard button. Here is a simple example using python-telegram-bot.
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, WebAppInfo
from telegram.ext import Application, CommandHandler, ContextTypes
BOT_TOKEN = "YOUR_BOT_TOKEN"
WEBAPP_URL = "https://your-domain.com/miniapp"
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
keyboard = [[
InlineKeyboardButton(
text="Open Shop",
web_app=WebAppInfo(url=WEBAPP_URL)
)
]]
await update.message.reply_text(
"Open the Mini App to place your order:",
reply_markup=InlineKeyboardMarkup(keyboard)
)
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.run_polling()
A few practical notes:
- The Mini App URL must be HTTPS.
- Telegram clients may cache or reload the page, so keep the app stateless at startup.
- You can also launch Mini Apps from menu buttons, inline keyboards, or deep links depending on your flow.
Step 2: Build the Mini App frontend
A Mini App can be as simple as a single HTML page. The key is to initialize the Telegram Web App SDK and read the runtime context.
Minimal HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Telegram Mini App Checkout</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<style>
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 16px;
background: var(--tg-theme-bg-color, #ffffff);
color: var(--tg-theme-text-color, #000000);
}
.card {
border: 1px solid rgba(0,0,0,0.08);
border-radius: 16px;
padding: 16px;
margin-bottom: 12px;
}
button {
width: 100%;
padding: 14px;
border: 0;
border-radius: 12px;
background: #2ea6ff;
color: white;
font-weight: 600;
font-size: 16px;
}
</style>
</head>
<body>
<h1>Choose a product</h1>
<div class="card">
<strong>Starter Pack</strong>
<p>$9.99</p>
</div>
<div class="card">
<strong>Pro Pack</strong>
<p>$19.99</p>
</div>
<button id="checkout">Checkout</button>
<script>
const tg = window.Telegram.WebApp;
tg.ready();
tg.expand();
// Optional: inspect useful context
console.log("initData:", tg.initData);
console.log("user:", tg.initDataUnsafe?.user);
document.getElementById("checkout").addEventListener("click", async () => {
const payload = {
productId: "starter-pack",
quantity: 1,
clientTime: Date.now()
};
const response = await fetch("/api/checkout", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
telegramInitData: tg.initData,
payload
})
});
const result = await response.json();
if (result.ok) {
tg.showAlert("Order created successfully.");
} else {
tg.showAlert(result.error || "Checkout failed.");
}
});
</script>
</body>
</html>
This gives you a working shell. Now let’s look at the parts that matter in real apps.
Step 3: Use Telegram theme and viewport APIs properly
Telegram Mini Apps feel best when they respect the client theme and viewport behavior.
Useful calls:
tg.ready();
tg.expand();
And useful theme variables include:
--tg-theme-bg-color--tg-theme-text-color--tg-theme-hint-color--tg-theme-button-color--tg-theme-button-text-color
If you want a production-quality app, do not hardcode all colors. Use Telegram theme values as CSS variables and provide sensible defaults.
If your app needs a bottom action button, Telegram’s MainButton can be better than a regular HTML button because it integrates with the client UI.
tg.MainButton.setText("Checkout");
tg.MainButton.show();
tg.MainButton.onClick(() => {
// trigger checkout
});
For a small checkout flow, a normal button is fine. For a multi-step mobile experience, MainButton often feels more native.
Step 4: Send data back to the bot or your backend
There are two common patterns:
- Send data directly to your backend from the Mini App.
- Use
sendData()to send a compact string back to the bot.
For practical business flows, backend submission is usually the better option because it supports structured payloads, validation, and server-side persistence.
If you want to close the Mini App and send a short message to the bot, you can use:
tg.sendData(JSON.stringify({ productId: "starter-pack" }));
But note the payload size is limited and the flow is more restrictive. For anything involving payment, order creation, or database writes, send the request to your server instead.
Step 5: Verify Telegram init data on the server
This is the most important part of the tutorial.
Telegram provides a signed init data string in tg.initData. Your backend must verify it using your bot token. If you skip verification, anyone can fake a request to your API.
Here is a Node.js example using crypto.
Node.js verification example
import crypto from "crypto";
function verifyTelegramWebAppData(initData, botToken) {
const urlParams = new URLSearchParams(initData);
const hash = urlParams.get("hash");
urlParams.delete("hash");
const dataCheckString = [...urlParams.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join("\n");
const secretKey = crypto
.createHmac("sha256", "WebAppData")
.update(botToken)
.digest();
const computedHash = crypto
.createHmac("sha256", secretKey)
.update(dataCheckString)
.digest("hex");
return computedHash === hash;
}
Then in your route:
import express from "express";
const app = express();
app.use(express.json());
const BOT_TOKEN = process.env.BOT_TOKEN;
app.post("/api/checkout", async (req, res) => {
const { telegramInitData, payload } = req.body;
if (!verifyTelegramWebAppData(telegramInitData, BOT_TOKEN)) {
return res.status(401).json({ ok: false, error: "Invalid Telegram data" });
}
const params = new URLSearchParams(telegramInitData);
const userJson = params.get("user");
const user = userJson ? JSON.parse(userJson) : null;
if (!user) {
return res.status(400).json({ ok: false, error: "Missing Telegram user" });
}
// Validate product ID on the server, never trust the client blindly.
const allowedProducts = {
"starter-pack": 999,
"pro-pack": 1999
};
if (!allowedProducts[payload.productId]) {
return res.status(400).json({ ok: false, error: "Invalid product" });
}
// Create order in your database here.
// Example response:
return res.json({
ok: true,
orderId: "ord_12345",
userId: user.id
});
});
app.listen(3000, () => console.log("Server running on :3000"));
A few implementation details matter here:
- Verify the signature before reading user identity.
- Parse the
userobject only after verification. - Re-check every client-selected product or price server-side.
- Use server-generated totals, not client-calculated totals.
Step 6: Handle the Telegram user object
After verification, Telegram user data typically gives you information like:
idfirst_namelast_nameusernamelanguage_codeis_premium
Use this data for identification and personalization, but do not treat it as a complete account system. Telegram identity is useful, but your application still needs its own database records, permissions, and order state.
A good pattern is:
- look up the Telegram user ID in your database
- create the user if it does not exist
- attach the current action to that account
- store a server-side order or session record
That makes future Mini App visits consistent and prevents duplicate records.
Step 7: Add a realistic checkout response
After creating the order, you may want to:
- show a success message in the Mini App
- open a payment provider flow
- return to the chat with a confirmation
- send the bot a message using the Bot API
Here is a simple client-side success path:
if (result.ok) {
tg.showPopup({
title: "Order created",
message: `Your order ${result.orderId} was created successfully.`,
buttons: [{ id: "ok", type: "default", text: "Done" }]
}, () => {
tg.close();
});
}
If you need to continue in chat after the Mini App closes, you can use your backend to message the user via the bot after the order is created.
Step 8: Common pitfalls in real Mini App projects
Here are the mistakes that cause the most trouble.
1. Trusting client-side data
Never accept price, user ID, or permissions from the browser without verification.
2. Forgetting HTTPS
Telegram requires secure hosting for web apps. Use proper TLS, even in staging if possible.
3. Ignoring theme and viewport behavior
If the interface looks broken inside Telegram, users will abandon it quickly. Test on mobile Telegram clients, not just desktop browsers.
4. Treating initDataUnsafe as trusted data
It is convenient for rendering the current user name, but not enough for authentication.
5. Making the app depend on a full login system
One of the strengths of Mini Apps is reducing account friction. Keep the entry flow simple and use Telegram identity where appropriate.
Step 9: A better production structure
For anything beyond a demo, separate your app into these parts:
- frontend: renders UI, reads Telegram SDK data, submits actions
- API: verifies Telegram init data and handles business rules
- database: stores users, orders, subscriptions, or sessions
- bot: launches the Mini App and sends follow-up messages
This separation makes it easier to evolve from a simple checkout into a larger product.
A practical folder layout might look like this:
miniapp/
frontend/
index.html
app.js
styles.css
backend/
server.js
telegram-auth.js
orders.js
bot/
bot.py
Step 10: Expand the tutorial into a real product flow
Once the checkout skeleton works, you can add features that matter to Telegram users:
- product catalog pulled from your API
- fast quantity selectors
- address or delivery note fields
- bot notifications for status changes
- saved preferences by Telegram user ID
- lightweight payment integration
The general rule is simple: keep the Mini App focused on one job. Telegram is best when the user can complete a task in a few taps, not when they must navigate a large website clone.
Final implementation checklist
Before shipping your Mini App, verify that:
- your bot opens the app through a web app button
- the app loads over HTTPS
- you call
tg.ready()andtg.expand() - you read
tg.initDataand send it to the backend - the backend verifies the init data hash
- user identity comes from verified Telegram data
- product IDs, prices, and totals are validated server-side
- the app gives a clear success or failure result
If all of that is in place, you have a real Mini App architecture, not just a web page embedded in Telegram.
Conclusion
Telegram Mini Apps are best when they make a chat flow more actionable. A checkout flow is a good example because it combines identity, a small amount of user input, and a server-side record of the result.
The key implementation takeaway is security: use Telegram’s signed init data for identity, but always verify it on your backend before processing any business logic. Once you get that right, you can build useful product flows that feel native to Telegram and stay lightweight enough to encourage completion.