mini apps

connect telegram mini apps to a yaebal bot through web app buttons, @yaebal/mini-app's initData validation, and bot api callbacks.

open a mini app

inline keyboards can open a web app url inside telegram. webAppUrl validates https (required in production) and merges deep-linking query params.

button.ts
import { createBot, InlineKeyboard } from "yaebal";
import { webAppUrl } from "@yaebal/mini-app";

const bot = createBot(process.env.BOT_TOKEN!);

bot.command("app", (ctx) =>
  ctx.reply("open the app", {
    reply_markup: new InlineKeyboard().webApp("open", webAppUrl("https://app.example.com")),
  }),
);

validate initData before trusting it

whether the mini app hands data back through a bot command or its own http backend, validate initData first — never trust user/chat/start_param before a validate* call has confirmed the signature. see @yaebal/mini-app for the full HMAC/Ed25519/ Authorization-header surface.

bot.ts
import { createBot } from "yaebal";
import { miniApp } from "@yaebal/mini-app";

const bot = createBot(process.env.BOT_TOKEN!).install(miniApp({ botToken: process.env.BOT_TOKEN! }));

bot.command("check", async (ctx) => {
  const initData = ctx.message?.text?.split(" ").slice(1).join(" ") ?? "";
  const result = await ctx.miniApp.validate(initData);
  if (!result.ok) return ctx.reply("rejected: " + result.reason);

  await ctx.reply("hi " + result.data.user?.first_name + "!");
});

most mini apps talk to their own web backend instead, via the Authorization: tma header convention:

server.ts
// your mini app's own http backend — not a bot update
import { validateAuthHeader } from "@yaebal/mini-app";

export default {
  async fetch(req: Request) {
    const result = await validateAuthHeader(req.headers.get("authorization"), process.env.BOT_TOKEN!);
    if (!result.ok) return new Response("unauthorized", { status: 401 });

    return new Response("hi " + result.data.user?.first_name);
  },
};

web app data messages

if the mini app calls Telegram.WebApp.sendData(), it arrives as message.web_app_data — client-controlled, so validate its shape like any other untrusted input (parseWebAppData in @yaebal/mini-app JSON-parses it for you).

web-app-data.ts
import { createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.on("message:web_app_data", async (ctx) => {
  await ctx.reply("mini app sent: " + ctx.message.web_app_data.data);
});

security checklist

  • validate initData on your backend before trusting user identity — ctx.miniApp.validate (HMAC) or ctx.miniApp.validateThirdParty (Ed25519, no bot token) from @yaebal/mini-app.
  • initData has no built-in expiry — @yaebal/mini-app defaults to rejecting anything older than 24h (maxAge), guarding against replaying a leaked-but-genuine payload.
  • do not put bot tokens in the mini app frontend.
  • treat web_app_data.data as untrusted input.
  • use short-lived server-side state for checkout or privileged actions.
  • log only safe user ids and action names, not full initData strings.

external domains

since july 20, 2026, telegram blocks mini app methods on external origins opened through links inside the app by default. keep method calls on the original mini app domain. this browser-side restriction does not change server-side initData validation. botfather's mini app offers an opt-out that leaves trust in linked sites to the bot developer. see the bot api 10.2 changelog.