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.