Telegram Mini App: How to Build, Launch, and Secure One
A Telegram Mini App is an ordinary web page. That sentence does most of the work in this article, because almost every mistake people make with Mini Apps comes from imagining something more exotic. There is no special runtime, no packaging step, no store review and no proprietary language. You write HTML and JavaScript, you host it wherever you host anything, and Telegram opens it in a frame with one extra object attached to the window.
What makes it a Mini App rather than a link is that object. It tells you who the user is, what the theme colours are, how tall the visible area currently is, and it gives you native buttons, native dialogs, haptics, cloud storage and a payment sheet. All of that arrives through a single script tag.
The part that decides whether your app is safe or trivially forgeable is one function, and most tutorials get it backwards. The identity Telegram hands you has to be verified on your server, using a signature whose secret key is derived in an order that looks wrong the first time you read it. That, plus the fact that two of the seven launch methods give you no identity at all, is the whole architecture question.
What a Mini App Actually Is
Start with what is not special, because that is most of it.
It is a web page you host
Telegram does not host your app, does not build it and does not review it. You give the platform a link to something you already run. Everything about deployment, uptime and cost stays exactly where it was before you had a Mini App, which is why the barrier to trying one is genuinely low.
The flip side is that everything which was your problem stays your problem. A slow server is a slow app, an expired certificate is a blank screen inside somebody's chat, and an outage is visible to every user who taps the button. There is no platform cache in front of you and no store to blame, which is worth knowing before a Mini App becomes the front door to a business.
The description in the documentation
The platform's own framing is that developers can use JavaScript to create infinitely flexible interfaces that can be launched right inside Telegram. Flexible is the operative word. The interface is yours; what Telegram supplies is the container, the identity of whoever opened it, and a set of native controls you can drive from your code.
Why it is not just a link
Because a link opens a browser and forgets who the user was. A Mini App opens inside the application, already knows which account is using it, matches the user's theme, and can hand a result back to the chat that opened it. The distance between those two experiences is the entire reason this feature exists.
There is a commercial version of the same point. A browser tab is a dead end for a messaging business, because the moment somebody leaves the conversation you lose the thread, the context and usually the sale. An app that opens in place keeps all three. That is why Mini Apps ended up carrying storefronts, dashboards and account panels rather than staying the novelty they looked like at launch, and it is why a bot with a serious interface almost always has one now.
Seven Ways to Launch One
The documentation defines seven distinct launch methods, and they are not interchangeable.
The button routes
A keyboard button opens a Mini App from the reply keyboard. An inline button opens one from a button attached to a message. The menu button beside the message field can be repointed from the default command menu to a Mini App instead, which is the most common setup for a bot whose main surface is an app rather than a conversation.
The profile and inline routes
A Main Mini App gets a prominent button on the bot's profile, which suits a bot that is essentially an app with a bot attached rather than the other way round. Inline mode can present one through a button above inline results, which is how an app reaches chats where the bot is not a member.
The link and attachment routes
A direct link opens an app straight from a web address in the form of the bot name followed by the app name. The attachment menu offers quick launch from inside any chat. Between them these two are how a Mini App escapes its own bot's chat and turns up where users already are.
Which one you should pick
Most projects need exactly one, and picking it early avoids rework, because the launch method decides what data you receive. If you are unsure, the menu button is the safest default for a private-chat app and the direct link is the safest for anything you intend to advertise, since a link can be posted anywhere.
The reason this matters more than it should is that the seven routes are not seven entrances to the same room. They differ in what the app knows, where it can appear, whether it can post back into a chat, and how much friction the client puts in front of the first launch. Two apps built identically but launched differently behave like different products, and nothing in the code makes that obvious.
The Split That Decides Your Architecture
This is the single most important table in the whole topic, and it is easy to state.
Five methods give you an identity
Inline button, menu button, Main Mini App, direct link and attachment menu launches all deliver initialisation data. That means your app receives a signed statement about who opened it, which you can verify and then trust. Anything that needs an account, a balance, a history or a permission has to launch through one of those five.
Note what that list is not. It is not a permission you enable, a setting you toggle or a plan you upgrade to. It is a property of the entrance the user walked through, decided at the moment they tapped, and your code has no say in it whatsoever.
Two methods give you nothing
Keyboard button launches and inline mode launches receive no initialisation data at all. They are anonymous by design. An app opened that way cannot know who is using it, which is not a bug and not something a clever workaround fixes.
What that means in practice
If you built a keyboard-button app and later needed to know the user, you are not adding a feature, you are changing the launch method and therefore the way people reach your app. That is why this decision belongs at the start. The anonymous routes are still useful, and there is a dedicated mechanism for getting data out of them, covered further down.
It also cuts the other way, and the anonymous routes are underrated because of it. An app that genuinely does not need an account is simpler, faster and easier to trust, since there is nothing to verify, nothing to store and no privacy conversation to have. Calculators, pickers, converters and configuration widgets are all better as anonymous apps, and building them with identity attached adds work and liability in exchange for nothing.
The One Script and the Object It Creates
Everything native comes from a single include, and it belongs in the head of your document.
The include
The script is served from Telegram's own domain and carries a version suffix. Self-hosting a copy is the wrong instinct here, because the file is versioned deliberately and a stale local copy silently strands you on an older interface with methods that do not exist on newer clients.
What appears on the window
Once the script runs you have a Telegram object with a WebApp inside it, and that is the whole surface. Fields describe the environment, including the Bot API version available, the platform name, the colour scheme, the theme colours, whether the app is expanded, and two separate viewport heights. Sub-objects give you the native controls.
Calling ready, and why it matters
There is a method that tells Telegram your interface has finished preparing itself. Until you call it, the client can keep showing its loading state over your app. It is one line, it is easy to forget, and forgetting it is the most common reason a working app looks broken for the first second of every launch.
The version field deserves a mention in the same breath. Newer controls and methods only exist on newer clients, and a user on an old build will simply not have them. Reading the reported version and checking before you call something is the difference between an app that degrades quietly on an old phone and one that throws an error nobody will ever report to you, because from their side it just did not work.
Validating the Data, and the Order Everyone Gets Wrong
This is where a Mini App is either secure or a joke, and the difference is about fifteen lines of server code.
Two fields, one of them a trap
The object gives you the initialisation data twice. One field is the raw query string, and it is the one you send to your server. The other is the same content already parsed into a convenient object, and its name contains the word unsafe for a reason that is not decorative.
Why the convenient one is unsafe
Because it is client-side data, and the client is the user. A parsed object sitting in a browser can be edited by anybody who opens developer tools. If your code reads a user identifier out of it and trusts that identifier, then every user of your app can become every other user of your app by typing a different number. That is not a theoretical attack, it is the first thing anybody curious will try.
It is worth picturing what the attack actually looks like, because it is not sophisticated. Somebody opens your app, finds the request your page makes to your server, sees an identifier in it, changes the number and sends it again. If your server answered the first request it will answer the second, and now they are reading somebody else's data. There is no exploit, no tooling and no skill involved; the whole attack is editing a field, which is exactly why the defence has to live on the server rather than in the page.
The signature check, step by step
The raw string carries a hash. You take every other field, sort them alphabetically, and join them as key and value pairs separated by line feeds, producing a data check string. You compute a signature over that string and compare it to the hash you were given. If they match, the data genuinely came from Telegram and nothing in it was altered on the way.
The part that looks backwards
The secret key for that signature is itself derived by signing your bot token with the constant string as the key. In other words the token is the message and the constant is the key, which is the reverse of what almost everybody writes on the first attempt. Getting this the wrong way round produces a signature that never matches, and the resulting bug looks like a platform problem rather than a two-argument mistake.
If your check never passes, that swap is the first thing to look at, ahead of anything else. The second is the string you are signing, because every field except the hash goes in, sorted, joined with line feeds, with the values exactly as they arrived rather than re-encoded. A single field left out, a different separator, or a value that has been decoded and re-encoded on the way through your framework all produce the same symptom, which is a signature that is confidently wrong.
Check the timestamp as well
The data carries a Unix timestamp recording when it was issued. A valid signature stays valid forever, so a signed payload captured today can be replayed next month unless you reject anything older than a window you choose. Verifying the signature and ignoring the timestamp leaves a real hole in an otherwise correct implementation.
Where the check belongs
On your server, every time, before anything else. Not once at login, not in the client, and not skipped because the request came from your own frontend. The token that makes this verification possible is the same credential everything else about your bot depends on, which is why our article on the Telegram bot token treats it as the crown jewel that it is.
The Buttons Telegram Draws for You
Three native controls come with the object, and using them is what separates an app that feels native from a web page in a frame.
The main button
There is a controllable primary button pinned to the bottom of the screen. It is the correct place for the single most important action on any screen of your app, and because Telegram draws it rather than your CSS, it sits below the keyboard, matches the user's theme and never fights the viewport.
The back button and the settings entry
A back control lives in the header and can be shown or hidden per screen, which is how you give a single-page app real navigation without inventing your own chrome. There is also a settings entry that appears in the app's own menu, which is the natural home for anything configuration-shaped.
Dialogs, haptics and closing confirmation
Alerts, confirmations and custom popups are available as native dialogs rather than browser ones, which matters because browser dialogs look wrong inside a chat application. There is haptic feedback for touch responses, and a closing confirmation you can enable so an accidental swipe does not discard a half-finished form.
Use them rather than rebuilding them
Every one of these has a hand-rolled equivalent somebody has written in CSS, and every one of those equivalents is worse. They break on keyboard open, they miss the safe areas on newer phones, and they ignore the user's theme. The native controls exist precisely so that you do not have to solve those problems again.
There is a subtler benefit that only appears once people use your app twice. Native controls sit where users already expect them, because every other Mini App on the platform puts them in the same place. A custom button in a custom position makes your app fractionally harder to use than every app next to it, and that cost is paid on every single interaction rather than once.
Theming, and Why Your App Looks Broken
A Mini App that ignores the theme is instantly recognisable, and not in a good way.
What you are given
The object exposes the current colour scheme as a simple light or dark value, and a set of theme parameters carrying the actual colours the user's client is using. Those are not suggestions. They are the palette of the application your app is sitting inside, and matching them is the difference between feeling built in and feeling embedded.
The failure mode
A hard-coded white background inside a dark client is jarring, and it is the single most common visual complaint about Mini Apps. It also tends to arrive late, because developers work in one theme and never test the other, and the client will happily open a blindingly bright app for a user who set dark mode two years ago.
The cheap fix
Read the theme parameters at start-up, write them into custom properties, and let your styles refer to those rather than to fixed colours. That is a small piece of work at the beginning of a project and an unpleasant retrofit later, which is a good reason to do it on day one.
Two habits make it stick. Test both schemes every time you touch the interface, because the failure is invisible in whichever one you personally use. And avoid hard-coding even the colours you take from the theme, since the palette is per client and per user rather than a fixed pair of light and dark values; treating it as data you read rather than a design you copy is what keeps the app correct on a client you have never seen.
The Viewport Problem
There are two height fields rather than one, and the reason is worth understanding before you fight it.
Two heights, not one
One field reports the currently visible height, which changes constantly as the user drags the app or the keyboard appears. The other reports the height in its stable state, ignoring transient gestures. Layout that follows the first will jitter; layout that follows the second stays still.
Expanding, and when to do it
An app can open at a reduced height and be expanded to full height programmatically. Expanding immediately on launch is usually right for anything with a form or a list, and usually wrong for a small confirmation-style app, where taking over the screen for a two-line question feels heavy.
What breaks if you ignore it
Fixed-position footers are the classic casualty. They sit correctly on your desktop browser and then float halfway up the screen on a phone with the keyboard open. The native main button avoids this entirely, which is the practical argument for using it instead of a bottom bar of your own.
Scrolling is the other place this shows up. An app that assumes it owns the full height will fight the container's own gesture handling, and the result is a screen that sometimes scrolls the app and sometimes drags the whole sheet. Expanding on launch, then laying out against the stable height, removes most of that conflict without any gesture handling of your own.
Cloud Storage, With Real Numbers
There is a small key-value store attached to each user, and its limits are published rather than guessed.
The published limits
Each bot can store up to one thousand and twenty-four items per user. A key can be one to one hundred and twenty-eight characters using letters, digits, underscores and hyphens. A value can be up to four thousand and ninety-six characters. Those numbers are generous for preferences and hopeless for anything resembling a database.
What it is genuinely good for
Draft state, interface preferences, a last-used selection, an onboarding flag, a small cache that survives the user reinstalling the application. Anything that would otherwise live in local storage and vanish belongs here instead, and it follows the user across devices for free.
What it is not
It is not your backend, it is not shared between users, and it is not a place to keep anything you would be unhappy to see edited, since it is written by the client on the user's device. Treat it as a convenience store rather than a system of record, and keep the truth on your server.
A useful rule of thumb is to ask what happens if a value in it is wrong. If the answer is that the user sees the wrong tab selected, it belongs there. If the answer is that somebody gets a balance they did not pay for, it does not. The limits are generous enough that the second kind of data will fit, which is precisely why the question has to be asked deliberately rather than settled by whether it fits.
Getting Data Back to the Bot
There are two mechanisms, they apply to different launch methods, and mixing them up wastes an afternoon.
The keyboard-button route
Apps launched from a keyboard button can send data back to the bot directly. The payload is capped at four thousand and ninety-six bytes, it arrives at the bot as a service message, and the Mini App closes when it is called. It is designed for small structured results, like a custom picker returning a chosen value.
The inline route
Apps launched from an inline button or the attachment menu use a different mechanism entirely, which sends a message on the user's behalf into the chat. It requires an identifier that arrives in the initialisation data, and it is how an app produces something visible in the conversation rather than something private to the bot.
Neither is your API
Worth stating plainly, because it catches people. Both of these are for handing a small result back to Telegram. Real application traffic, meaning your data, your queries and your business logic, goes from your page to your own server over ordinary requests, exactly as it would in any other web application. The Telegram mechanisms are the last mile, not the road.
Choosing between them
If the outcome belongs to the bot, use the direct route and accept the keyboard-button constraint. If the outcome belongs in the chat where people can see it, use the inline route. If the outcome belongs to your own system, use neither and call your own endpoint, which is what most serious apps do most of the time.
One consequence catches people out. Both Telegram routes close the app when they fire, so neither is suitable for anything the user should continue afterwards. If you want a confirmation screen, a receipt or a next step, that has to happen before you hand the result back, because once it is sent the interface is gone and there is nothing left to show it in.
Direct Links and the startapp Parameter
The link form is the most useful distribution mechanism in this feature, and it carries a parameter that turns any placement into a measurable one.
The link shape
A direct link addresses the bot and then the app by name, which means an app can be opened without the user ever having installed anything or opened a conversation. That is what makes Mini Apps shareable in a way ordinary bot interfaces are not; the same property our piece on Telegram invite links covers for groups.
The parameter it carries
A query parameter on that link is passed through to the app as a start parameter when it opens. Different codes for different placements turn your app into something you can attribute, because the very first load already carries its source. The same link can also request permission to message the user, and it can carry a display mode asking for a compact or full-screen presentation.
Context follows the link
When somebody opens a direct link from inside a chat, that chat's context travels with the launch rather than defaulting to the bot. This is what allows a Mini App to behave differently depending on where it was opened from, which is the mechanism behind apps that act on the current group.
That behaviour is also a trap if you assume it. An app opened from a group and the same app opened from a profile receive different context, so code that reads the context without checking which case it is in will work in testing and fail in the wild. Handling the bot case first and treating the chat case as an enhancement is the order that survives contact with real users. Our guide on how to automate a Telegram channel covers the same context problem from the posting side.
Payments Inside a Mini App
Selling from inside a Mini App is well supported, and the rules about currency are stricter than most people expect.
Opening a payment sheet
An invoice link can be opened from inside the app, which presents a native payment interface. Your code receives an event when that interface closes, carrying the outcome, so the app can react to a completed, cancelled, failed or pending payment without polling anything.
The currency rule for digital goods
For digital goods the platform requires transactions to be carried out in its own currency, with a specific currency tag, and the reason is external: mobile app store rules forbid third-party payment processing for digital items. This is not a preference you can design around. Physical goods are different and keep the ordinary provider arrangement.
What changes practically
For digital goods the provider token can be left empty, because there is no external processor involved. Refunds are issued through a dedicated method rather than through a payment provider's dashboard, which means your support process lives in your own code. Our articles on Telegram Stars and the Telegram wallet cover the currency side in detail.
Where this fits a real business
A Mini App with payments is the shortest path from a message to a completed sale that exists on the platform, because nothing leaves the application. If you are weighing that against the alternatives, our guide on how to sell on Telegram compares it with the routes that send people to a website.
The support side deserves planning before launch rather than after the first complaint. Because refunds are issued through your own code rather than a provider's dashboard, somebody has to be able to issue one at eleven at night without a deployment. Building a small internal path for that on day one costs an hour, while discovering you need it during a dispute costs considerably more, and our piece on Telegram monetization covers where the money actually lands.
Permissions You Have to Ask For
A Mini App does not silently acquire anything, and the two requests it can make are worth planning.
Permission to write
An app can request the right to send the user messages, which matters because a bot generally cannot start a conversation with somebody who has never opened one. Asking at the right moment, meaning after the app has demonstrated it is useful rather than on the first screen, is the difference between a granted request and a dismissed one.
The contact request
An app can also ask for the user's phone number. This is a heavier ask than it looks, since it converts an anonymous session into a real identity, and it should be reserved for cases where you genuinely need it. Requesting it for no visible reason is one of the fastest ways to lose a user mid-flow.
The confirmation the client shows
Separately from your requests, the client itself asks for confirmation before opening a Mini App from a bot the user has not interacted with, except for a whitelist. That is a protection against a link opening an unknown app, and it means the first launch from a cold audience always includes a friction step you do not control.
Plan the first screen around that. Somebody arriving from a link has just clicked through a warning about an unfamiliar bot, which means they open your app already slightly on guard. An interface that immediately explains what this is and what it will do converts far better than one that opens on a login prompt, and it costs nothing but ordering.
Where a Mini App Is the Wrong Tool
The feature is genuinely good, which is why it gets used for things it should not be.
When the interaction is one question
If your entire interface is a question with three answers, buttons on a message are better than an app. They are faster to open, they work on every client, they leave a visible record in the chat, and they need no hosting. Reaching for an app there adds a loading screen to something that was instant.
The same applies to anything that is really a form with four fields. A short conversation handled by the bot itself, one question at a time, is often faster for the user and always cheaper for you, and it works on clients where an app would not open at all. Our article on the Telegram auto reply bot covers that conversational shape and where its own limits start.
When you need to reach people first
A Mini App is something a user opens. It is a pull, and it can do nothing at all until somebody launches it. Anything outbound runs on completely different machinery, which is what our Mass DMs tooling and our guide to the Telegram broadcast bot are about, and no amount of Mini App work substitutes for it.
When the work happens on accounts, not bots
Mini Apps belong to bots. If the operation you actually need runs on user accounts, such as sourcing members or mirroring a channel, none of this applies, because ordinary accounts have no Mini Apps and no bot interface. That is the line our Member Adder and Channel Clone products sit on, and it explains why they look nothing like a bot even when the goal is similar.
If You Run This at Volume
One app is a project. Several, or one with real traffic, is an operation.
Every app is a bot with a credential
There is no Mini App without a bot behind it, which means every app you ship is another token to hold, another command surface to keep consistent and another identity to keep alive. Our Bot Manager product exists for that side of it, and our article on Telegram bot commands covers the surface that sits next to the app.
Validation cost is real at scale
Every request from every user should be verified, and that verification is a signature computation per call. It is cheap individually and not free in aggregate, so validating once and issuing your own short-lived session is the normal pattern rather than re-checking the same payload on every request forever.
Short-lived is the operative phrase there. The whole point of the timestamp check is to stop an old payload being replayed, and issuing a session that never expires quietly gives that property back. A session measured in hours rather than months keeps the guarantee you paid for and costs the user nothing, since reopening the app produces fresh initialisation data anyway.
The app is one surface among several
A Mini App handles the people who already arrived. Finding them is a different problem with different tools, which is what ProspectPulse scans for, and running the accounts behind any outbound work is what Account Manager handles. Treating the app as the whole funnel is the mistake that leaves a beautiful interface with nobody in it.
What All of This Adds Up To
A Mini App is a web page you host, opened inside Telegram, with one object attached that supplies identity, theme, viewport, native controls, cloud storage and payments. Nothing about building one requires new skills, and almost nothing about it is proprietary except the object.
Two decisions carry the weight. The launch method, because five of the seven give you a verified identity and two give you nothing at all, and changing your mind later means changing how people reach you. And the validation, because the identity you are handed is only worth anything after your server has checked the signature, using a secret derived by signing the token with the constant rather than the other way round, and after you have rejected anything too old to trust.
The rest is craft. Match the theme, follow the stable viewport rather than the moving one, use the native buttons instead of rebuilding them, keep cloud storage for preferences and the truth on your server, and remember that an app is something people open rather than something that reaches them. Get those right and a Mini App stops feeling like a page in a frame and starts feeling like part of the application it opened in.
Frequently Asked Questions
What is a Telegram Mini App?
A web application written in ordinary HTML and JavaScript that opens inside Telegram rather than in a browser. You host it yourself and Telegram loads it in a frame, attaching an object that tells your page who opened it, what the theme colours are, how tall the visible area is, and giving you native buttons, dialogs, storage and payments.
How do I know which user opened my Mini App?
Through the initialisation data, but only if the app was launched by a method that provides it. The data arrives twice, once as a raw string and once already parsed. Send the raw string to your server, verify its signature there, and only then trust what it says. The parsed version is convenience data and can be edited by the user.
Why is initDataUnsafe called unsafe?
Because it lives in the browser and the browser belongs to the user. Anybody can open developer tools and change the values in it. If your code reads a user identifier out of that object and acts on it without verifying the signature on the server, every user of your app can impersonate every other user by editing one number.
How do I validate initData?
Take every field except the hash, sort them alphabetically, and join them as key and value pairs separated by line feed characters. Compute a signature over that string and compare it with the hash you received. The secret key for that signature is produced by signing your bot token with the constant string as the key, which is the reverse of the order most people try first.
Which launch methods do not give me the user?
Two of the seven. Apps launched from a keyboard button and apps launched through inline mode receive no initialisation data at all and are anonymous by design. The other five, meaning inline button, menu button, the profile app, direct links and the attachment menu, all provide it.
How much can a Mini App store per user?
Up to one thousand and twenty-four items for each user of each bot. Keys can be one to one hundred and twenty-eight characters using letters, digits, underscores and hyphens, and values can be up to four thousand and ninety-six characters. It suits preferences and draft state, not records you rely on.
The per-user part is easy to misread as well. The quota belongs to each pairing of bot and user rather than to your app as a whole, so a thousand users do not share a thousand items; they have a thousand each. That is generous, and it is still the wrong place for anything two users need to agree on.
How do I send data from the app back to the bot?
If the app was launched from a keyboard button there is a method that sends up to four thousand and ninety-six bytes to the bot as a service message and closes the app. If it was launched from an inline button or the attachment menu, a different method sends a message into the chat on the user's behalf. Everything else should go to your own server.
Can a Mini App take payments?
Yes, by opening an invoice from inside the app, which presents a native payment sheet and reports the outcome back to your code when it closes. For digital goods the platform requires its own currency, because app store rules forbid third-party processing of digital items, and in that case no external provider token is needed.
What is the startapp parameter for?
It is a value carried on a direct link and handed to your app as a start parameter when it opens. Using a different code per placement means the first load already tells you where the user came from, which turns any link you publish into something you can measure without adding tracking of your own.
Why does my Mini App look wrong in dark mode?
Because it is ignoring the theme. The object reports the current colour scheme and the actual palette the client is using. If your styles use fixed colours instead of those values, your app will be bright inside a dark client and will look pasted in rather than built in. Reading the theme once at start-up and mapping it to custom properties fixes it.
Do I need to publish my Mini App anywhere?
No. There is no store, no review and no packaging step. You host the page yourself and point a bot at it, which is why a working prototype can exist the same afternoon you start. The trade is that uptime, performance and cost remain entirely your responsibility.
An App Waits. Outreach Does Not.
A Mini App only works once somebody opens it. Reaching people in the first place is a different machine entirely, and it is the one our tooling is built on. Open a demo and see it.
Try Free Demo