How to Test a Telegram Mini App: From Browser Mock to Real Telegram
To test a Telegram Mini App, start in a normal browser with a mocked Telegram.WebApp, then serve your local build over HTTPS, register the URL with @BotFather and open it inside Telegram with DevTools attached. The final step happens on the server: validate initData before you trust the user. Below is the whole workflow, from localhost to a production checklist.
Step 1: test the UI in a normal browser
Outside Telegram there is no client behind Telegram.WebApp: no launch data, no theme, no native buttons. You can still build most of the interface in a desktop browser if your code tolerates that:
const tg = window.Telegram?.WebApp;
const insideTelegram = Boolean(tg?.initData);
if (insideTelegram) {
tg.ready(); // hide the loading placeholder as early as possible
tg.expand();
} else {
console.info("Outside Telegram: using mock data");
}
Style the page with the theme variables and a fallback, for example var(--tg-theme-bg-color, #fff), so it stays readable in both environments.
To see what each API call does without writing code, open the Mini App Tester. In a regular browser it runs in mock mode and emulates Telegram.WebApp: press MainButton and BackButton, open popups, write to CloudStorage, switch themes and watch 45+ events in the log. Mock data is only a stand-in, though: its initData is not signed by Telegram and will (correctly) fail server-side validation.
Step 2: run the Telegram Mini App from localhost over HTTPS
Telegram opens Mini Apps from HTTPS URLs only (the test environment is the exception), and your phone can't reach http://localhost anyway. The standard way to test a Telegram Mini App on localhost is a tunnel that gives your dev server a public HTTPS address:
# start your dev server, e.g. Vite on port 5173
npm run dev
# in a second terminal, use one of the tunnels
ngrok http 5173
cloudflared tunnel --url http://localhost:5173
Each tool prints a public https:// URL. Two common snags:
- Vite rejects the tunnel host. Recent Vite versions block unknown hosts on the dev server; add the tunnel domain to
server.allowedHostsinvite.config.js. - The address changes. Many tunnels assign a new random URL on every restart. When that happens, update it in @BotFather or repeat the
setChatMenuButtoncall from Step 3.
Step 3: register the Mini App with @BotFather
A Mini App always belongs to a bot. If you don't have one yet, follow how to create a Telegram bot. Then choose how users launch your URL:
- Direct link: send
/newappto @BotFather, pick the bot and follow the prompts. The app then opens athttps://t.me/<bot_username>/<app_name>. - Menu button: in @BotFather use
/setmenubuttonor Bot Settings β Menu Button, or call thesetChatMenuButtonmethod. - Keyboard buttons: a
web_appbutton in a reply keyboard or in an inline keyboard (private chats only).
curl -X POST "https://api.telegram.org/bot123456789:AAEexampleTokenReplaceWithYours/setChatMenuButton" \
-H "Content-Type: application/json" \
-d '{"menu_button": {"type": "web_app", "text": "Open app",
"web_app": {"url": "https://your-tunnel.example.com"}}}'
Without chat_id this sets the default menu button for all users. Whenever your tunnel URL changes, repeat the call in setChatMenuButton in the Bot API Tester instead of retyping curl.
Step 4: use the Telegram test environment
Telegram runs a separate test environment where Mini App links may use plain HTTP without TLS, which is handy for testing on a local network. It requires its own account and its own bot:
- iOS: tap the Settings icon 10 times β Accounts β Login to another account β Test.
- Telegram Desktop: β° Settings β Shift+Alt+right-click "Add Account" β Test Server.
- macOS: click the Settings icon 10 times to open the Debug Menu, then β+click "Add Account".
Create a bot with @BotFather inside the test account and send API calls to https://api.telegram.org/bot<token>/test/METHOD_NAME. Flood limits are not raised there and can be stricter, so it is not a place for load tests.
Step 5: debug the Mini App inside Telegram
Many bugs that never appear in Chrome show up only in the real WebView. Telegram's official Debug Mode for Mini Apps instructions, in short:
| Platform | How to inspect |
|---|---|
| iOS | Tap the Settings icon 10 times and enable Allow Web View Inspection; connect via USB and open Develop β your device in Safari on a Mac. |
| Android | Enable USB debugging; in Telegram Settings scroll to the bottom, press and hold the version number twice, choose Enable WebView Debug; open chrome://inspect/#devices in Chrome. |
| Telegram Desktop (Windows, Linux) | Install the beta version; Settings β Advanced β Experimental settings β Enable webview inspection; right-click β Inspect. |
| Telegram for macOS | Install the beta version; click the Settings icon 5 times and enable Debug Mini Apps; right-click β Inspect Element. |
To compare clients quickly, set https://telegram-bot-api.info/telegram-mini-app-tester.html as the Mini App URL of a test bot. Inside Telegram the Mini App Tester uses the real API and shows version, platform, colorScheme, themeParams, safe areas and every event a given client fires. In your own code, gate newer features with Telegram.WebApp.isVersionAtLeast("8.0") instead of assuming everyone runs the latest app.
Step 6: validate initData on the server
Telegram.WebApp.initDataUnsafe is for display only. Send the raw Telegram.WebApp.initData string to your backend and verify it:
- Parse the query string and remove
hash. - Sort the remaining fields alphabetically and join them as
key=valuelines separated by\n: this is thedata_check_string. - Compute
secret_key = HMAC_SHA256(key="WebAppData", message=bot_token). - The data is genuine if
hex(HMAC_SHA256(key=secret_key, message=data_check_string))equalshash. - Reject stale data by checking
auth_date.
import crypto from "node:crypto";
export function validateInitData(initData, botToken, maxAgeSec = 3600) {
const params = new URLSearchParams(initData);
const hash = params.get("hash") ?? "";
params.delete("hash");
const dataCheckString = [...params.keys()].sort()
.map((key) => `${key}=${params.get(key)}`)
.join("\n");
const secretKey = crypto.createHmac("sha256", "WebAppData").update(botToken).digest();
const expected = crypto.createHmac("sha256", secretKey).update(dataCheckString).digest("hex");
const valid = hash.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expected));
const age = Date.now() / 1000 - Number(params.get("auth_date"));
if (!valid || !(age <= maxAgeSec)) return null;
return { ...Object.fromEntries(params), user: JSON.parse(params.get("user") ?? "null") };
}
On the client, post the string unchanged, e.g. fetch("/api/session", { method: "POST", body: Telegram.WebApp.initData }), and validate with the token of the bot that launched the app. A third party that must not know your token can instead verify the signature field with Telegram's Ed25519 public key, as described in the official docs.
Bot API 10.2: Mini App origin protection
Bot API 10.2 disallowed calling Mini App methods from origins other than the original Mini App domain, and the protection was enabled for all Mini Apps on July 20, 2026. During testing this bites when the app redirects from the registered tunnel URL to another host, such as a staging domain or a login page: Telegram.WebApp calls stop working there. Keep the code that talks to Telegram on the registered domain and re-register the URL whenever the tunnel host changes. You can opt out in the @BotFather Mini App, but then it is up to you to make sure the Mini App has no links to untrusted sites.
Telegram Mini App testing checklist
- The UI works in mock mode and in both light and dark themes.
Telegram.WebApp.ready()is called as soon as the essential UI is rendered.- The URL in @BotFather or
setChatMenuButtonmatches your current tunnel or production domain. - Tested on iOS, Android and desktop with DevTools; newer features gated by
isVersionAtLeast(). - Fullscreen layouts respect
safeAreaInsetandcontentSafeAreaInset. - The backend validates
initData(hash andauth_date) and never trustsinitDataUnsafe. - All
Telegram.WebAppcalls run on the registered origin. - Production runs on your own domain with a valid certificate, not on a temporary tunnel.
Your validation code depends on the bot token, so keep it secret: see how to get and protect a bot token. If the same backend also receives bot updates, continue with how to set up a Telegram bot webhook.
Frequently asked questions
How do I test a Telegram Mini App on localhost?
Telegram opens Mini Apps only from HTTPS URLs, so expose your dev server through an HTTPS tunnel (for example ngrok or Cloudflare Tunnel) and register that URL in @BotFather or with setChatMenuButton. In the test environment plain HTTP links are allowed.
Can I test a Telegram Mini App without Telegram?
Partly. A normal browser has no Telegram client, so you need a mock of Telegram.WebApp, such as the mock mode of the Mini App Tester. Real initData, native popups and haptics only work inside Telegram.
How do I open DevTools for a Telegram Mini App?
On iOS enable Allow Web View Inspection (tap Settings 10 times) and use Safari's Develop menu. On Android enable WebView Debug in Telegram's debug settings and open chrome://inspect/#devices. On desktop and macOS use the beta apps' webview inspection options.
Why does my initData validation fail?
Usual causes: validating initDataUnsafe instead of the raw initData string, using another bot's token, not URL-decoding values, leaving hash in the data-check string, swapping the HMAC key and message, or testing with mock data that Telegram never signed.
Why do Telegram.WebApp methods stop working on another domain?
Since Bot API 10.2, Mini App methods are blocked when called from an origin different from the Mini App domain; the protection was enabled for all Mini Apps on July 20, 2026. Keep the app on its registered domain or opt out in @BotFather.
Is there a separate test server for Telegram Mini Apps?
Yes. The test environment is separate from production: you need a new account and a new bot there, Bot API calls go to https://api.telegram.org/bot<token>/test/METHOD_NAME, and Mini App links may use HTTP without TLS.
Related guides
How to Create a Telegram Bot: Step by Step with BotFather and Python
How to create a Telegram bot step by step: register it with @BotFather, get the token, test it with getMe and send a first message with curl or Python.
Telegram Bot API 10.0β10.3: Rich Messages, Ephemeral Messages, Guest Mode and More
What's new in Telegram Bot API 10.0β10.3: guest mode, rich messages, streamed drafts, ephemeral messages, join request queries, with JSON examples.
Telegram Bot API Limits: Rate Limits, 429 Errors, File Sizes and Message Length
Telegram Bot API rate limits explained: 30 msg/s, 1 msg/s per chat, 20/min in groups, handling 429 retry_after, plus file size and message length limits.
How to Send a Message with the Telegram Bot API (sendMessage)
Telegram Bot API send message guide: sendMessage with curl, Python and JavaScript, HTML and MarkdownV2 formatting, inline buttons and error fixes.
Telegram Bot Token: How to Get It, Find It Again, Check It and Keep It Safe
Telegram bot token guide: get one from @BotFather, find the token of an existing bot, check it with getMe, revoke a leaked token and store it safely.
How to Set Up a Telegram Bot Webhook: setWebhook, Secret Token and Troubleshooting
Set up a Telegram bot webhook: setWebhook URL requirements, secret_token header check, getWebhookInfo debugging, deleteWebhook and fixes for n8n.
How to Get a Telegram Chat ID for a User, Group, Channel or Topic
How to get a Telegram chat ID for a user, group, supergroup, channel or forum topic with getUpdates, plus fixes for an empty result and 409 Conflict.