· 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:

  1. The user chats with your bot.
  2. The bot sends a button that opens the Mini App.
  3. Telegram loads your web app inside the client.
  4. Your app reads Telegram.WebApp.initData and Telegram.WebApp.initDataUnsafe.
  5. The user completes an action.
  6. Your app sends the action plus the signed init data to your backend.
  7. 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:

  1. Send data directly to your backend from the Mini App.
  2. 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 user object 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:

  • id
  • first_name
  • last_name
  • username
  • language_code
  • is_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:

  1. look up the Telegram user ID in your database
  2. create the user if it does not exist
  3. attach the current action to that account
  4. 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() and tg.expand()
  • you read tg.initData and 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.

← All posts