One Inline Button for Everyone: Parameters, Authentication, and Callbacks in a Telegram Mini App
Imagine walking into a room where a single button on the wall opens the door to a completely different room for each person who presses it. That’s not science fiction – it’s exactly what a Telegram Mini App can do. One inline button. Same public URL. Yet every user sees their own dashboard, with their own name, progress, and actions. The secret? Parameters, authentication, and callbacks buried in a tiny piece of JSON.
It’s easy to think of an inline button as a purely visual element: a blue rectangle that either says “Open” or “Pay.” But in the Telegram universe, the button is a full-fledged messaging endpoint. When a user taps it, Telegram doesn’t just open a web page. It orchestrates a handshake that transfers identity, context, and a return channel – all within milliseconds.
In this article, we’ll dissect the three layers that make a single button work for an entire user base. You’ll learn how to pass parameters without losing security, how Telegram’s token-based authentication works under the hood, and how to implement callbacks that keep your bot and Mini App in perfect sync. By the end, you’ll be able to build scalable, “one button fits all” experiences on Telegram.
A Quick Tour of Telegram Mini Apps
Telegram introduced Web Apps in 2021 as a way to embed rich, interactive HTML inside the chat interface. After the 2023 rebranding, they became Mini Apps – lightweight, full-screen web applications that run directly within Telegram’s app. Unlike a simple bot message, a Mini App can have complex navigation, forms, and even crypto wallets.
How does a Mini App get launched? Through one of several entry points:
- The bot’s menu button (top left).
- An inline keyboard button with
web_apptype. - A deep link like
https://t.me/YourBot/YourApp?startapp=param. - A custom keyboard button that references an existing Mini App.
The most flexible entry point for a one-button flow is the inline keyboard button. It appears directly in your bot’s message, can be combined with other buttons, and doesn’t require the user to discover a bot menu. When the user taps it, Telegram opens the Mini App in a dedicated view, and the Mini App receives a special query string called initData.
That initData is the heart of the entire system. It’s the reason you don’t need a login form, a session cookie, or a magic link. Telegram already knows who the user is, and it signs that knowledge so your server can trust it.
The Anatomy of an Inline Button
An inline button in Telegram Bot API is defined as an InlineKeyboardButton object. It has a large number of fields, but only one can be set per button. Here is the complete map as of 2026:
| Field | What it does | Best for |
|---|---|---|
url |
Opens an external HTTPS URL in the browser (not in the Telegram UI) | Simple web links |
callback_data |
Sends a callback_query update to the bot |
Server-side actions after press |
web_app |
Opens a Mini App from a WebAppInfo object |
Full-screen app experiences |
login_url |
Displays a log-in button for your website | OAuth or session-based auth |
switch_inline_query |
Switches the user to inline mode | Inline query suggestions |
pay |
Starts a Telegram Payment bot | Payments |
For our one-button pattern, the star is web_app. The web_app field accepts a WebAppInfo object, which contains a single url property. Telegram will fetch that URL and render it as a Mini App. Here’s a simple example in JSON:
{
"text": "Open My App",
"web_app": {
"url": "https://yourapp.com/app"
}
}
In Python, using the popular python-telegram-bot library, you’d write:
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
button = InlineKeyboardButton("Open App", web_app={"url": "https://yourapp.com/app"})
keyboard = InlineKeyboardMarkup([[button]])
await context.bot.send_message(chat_id=chat_id, text="Press me", reply_markup=keyboard)
The button is no different for Alice or Bob. It carries no user identifier. So how does the mini app know that Alice is Alice? The answer lies in the initData that Telegram automatically appends to the Mini App’s global window.Telegram.WebApp object.
Passing Parameters: The Static URL Myth
Many developers make a mental model: “The button URL is the same, so there’s no way to pass user-specific parameters.” That’s true if you try to stuff something into the button definition. But parameters can be divided into two categories: static and dynamic.
Static Parameters in the URL
You can embed generic parameters in the web_app URL right in the button code. For instance:
url = "https://yourapp.com/app?source=inline_button&origin=blog"
Every user who presses the button will see the same URL. The Mini App’s front end can parse window.location and use these values as configuration. For example, the value origin tells the backend which channel drove the user in. This is useful for analytics and A/B tests.
Dynamic Parameters via Deep Links
If you want a different parameter for each user, you need a deep link. Deep links are special URLs that start with https://t.me/YourBot/YourApp?startapp=.... When a user opens that link, Telegram launches the Mini App and sets the startapp parameter in the Mini App’s initData.
However, there’s an important distinction: deep links are not inline buttons. They are placed in messages, on websites, or in QR codes. The inline button, in contrast, always has a fixed URL. So if your button must be dynamic, you would need to generate a new keyboard for each user – which violates the “one button” ideal.
The Real Workaround: initDataUnsafe
Here’s the pragmatic solution: don’t rely on URL parameters for personalized data. Rely on initDataUnsafe, which is parsed on the front end. It contains the user object, even when the button is shared. The Mini App can extract user.id and send it to your backend via an HTTPS request. A single static button now supports unlimited personalization because the originating user ID is always available.
To illustrate, the following JavaScript snippet reads the user info:
const tg = window.Telegram.WebApp;
const dataUnsafe = tg.initDataUnsafe;
const userId = dataUnsafe?.user?.id;
const startParam = dataUnsafe?.start_param; // empty for inline button
console.log(`User ${userId} opened the app`);
This is exactly how the “one button for everyone” pattern works in production: the button URL is simple, the front-end extracts the user ID, and the back-end personalizes the response.
Authentication: No Password. No Token. Just Hash.
The initData string is not just a static JSON blob. It’s a signed query string containing:
user– JSON with user.info.auth_date– Unix timestamp of when the data was created.query_id– unique ID for the current launch.hash– HMAC-SHA256 signature of the other fields.
Here’s a typical raw initData as you might see it in the browser:
query_id=AAHl...&user=%7B%22id%22%3A123456789%2C%22first_name%22%3A%22Alex%22%7D&auth_date=1755100800&hash=3b6e...
Validating the Hash
To build a secure “everyone but only authenticated users” flow, your backend must validate the hash. The official procedure (from Telegram’s Web App documentation) is simple:
- Parse
initDatainto a list of key–value pairs. - Remove the
hashpair. - Sort the remaining pairs by key alphabetically.
- Construct a data-check string by concatenating each pair as
key=value, separated by line breaks\n. - Compute the secret key:
HMAC_SHA256(bot_token, key="WebAppData"). - Compute the final hash:
HMAC_SHA256(data_check_string, secret_key). - Compare this to the
hashfrom the original data.
Here’s a working Python function. We’ll use only standard libraries so you can drop it into any project:
import hashlib
import hmac
from urllib.parse import parse_qsl
def validate_init_data(init_data: str, bot_token: str) -> bool:
# 1. Parse the query string
parts = parse_qsl(init_data)
hash_val = dict(parts).get("hash", "")
# 2. Filter out the hash
filtered = [kv for kv in parts if kv[0] != "hash"]
# 3. Sort by key
filtered.sort(key=lambda kv: kv[0])
# 4. Data-check string
data_check_string = "\n".join(f"{k}={v}" for k, v in filtered)
# 5. Secret key derived from bot token
secret_key = hmac.new(
b"WebAppData", bot_token.encode(), hashlib.sha256
).digest()
# 6. Final HMAC
computed_hash = hmac.new(
secret_key, data_check_string.encode(), hashlib.sha256
).hexdigest()
# 7. Compare in a time-safe way
return hmac.compare_digest(computed_hash, hash_val)
Once this function returns True, you can trust user.id to identify the user. If it returns False, the request is forged and should be dropped.
Common Pitfalls
- Using
initDataUnsafedirectly on the backend. The front end can be compromised; send the rawinitDatato your server and validate there. - Forgetting
auth_date. A valid signature doesn’t mean the data is fresh. Telegram recommends rejectinginitDataolder than 24 hours. For sensitive actions, use a 5-minute window. - Hashing with wrong keys. The secret key is derived from the bot token as
HMAC-SHA256(bot_token, key="WebAppData"). Some developers mistakenly use the bot token directly as the HMAC key.
Callbacks: What Happens After the Button Is Pressed?
Once the user presses the inline button and the Mini App opens, the conversation doesn’t end. A complete “one button” architecture needs a return path. The term “callback” can mean different things in Telegram, so let’s break them apart.
1. The Traditional Bot API CallbackQuery
When a user presses an inline button that has callback_data, Telegram sends a callback_query update to your bot. You can then edit the message, show a popup, or trigger a server-side action. The web_app button does not lead to a callback_query; it simply opens the Mini App. If you want a callback tone, you need to define another inline button later—for example, after the user completes an action in the Mini App.
2. The Mini App-to-Backend Callback
Your Mini App is a web app, so it can talk directly to your backend over HTTPS. The classic pattern: the front-end receives initData, sends it to your backend with the desired action, and the backend validates the hash and returns JSON.
Example frontend fetch:
fetch('https://api.yourapp.com/launch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ initData: tg.initData })
})
.then(res => res.json())
.then(data => {
// personalized data from backend
});
This is a callback – your server gets a request that originates from the Mini App environment. It’s not a Telegram update, but it’s still part of the button lifecycle.
3. The WebApp.sendData Method
Telegram’s JS SDK provides a method called WebApp.sendData(data). When you call it, the Mini App closes and sends the data string to the bot as a chat message. The bot receives it as a regular message update with the type web_app_data.
Here’s how it works:
window.Telegram.WebApp.sendData(JSON.stringify({ action: "survey", score: 95 }));
On the bot side, you’ll see an update like:
{
"message": {
"message_id": 1234,
"web_app_data": {
"data": "{\"action\":\"survey\",\"score\":95}"
}
}
}
This is useful when the user’s action should appear in the chat (e.g., “I just completed a level”). sendData is a safety valve: unlike AJAX, it doesn’t require the user to keep the Mini App open, and it delivers the result directly to Telegram.
4. Direct Bot API Messages
After the user closes the Mini App, your backend can use the Bot API to send a message, edit an existing one, or even send a new inline keyboard. This is how you extend the flow beyond a single interaction.
For example, after the Mini App saves a user preference, the backend might send a message:
await bot.send_message(
chat_id=user_id,
text="Preference saved!",
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("More settings", callback_data="settings")]])
)
Now the user sees a new button with callback_data, and when they tap it, the bot receives a callback_query. That’s a clean callback mechanism for the next step.
The One Button Pattern in Production
Let’s put everything together with a realistic scenario: a bot that gives each user a personalized daily briefing.
-
The button: Every chat receives the same message:
"Open your briefing", with aweb_appbutton pointing tohttps://briefing.example.com. The URL is static. -
The launch: When Alice presses the button, the Mini App opens. The front end calls
tg.initDataUnsafe.user.idand gets12345. -
The authentication: The front end sends a POST with
initDatato the backend. The backend validates the hash and retrieves Alice’s profile. It sees that her preferred time is 9 AM and her interests are[tech, crypto]. -
The personalization: The backend returns JSON with today’s news items, adjusted for Alice’s timezone and interests.
-
The callback: After Alice reads for 2 minutes, she taps “Mark as read” inside the Mini App. The Mini App sends a POST request to
/mark_readwithinitData. The backend validates again, updates the database, and sends a Telegram message: “Great! You’ve read tonight’s digest. See you tomorrow.”
If Alice closes the Mini App before marking, the backend can send a follow-up message with an inline button using callback_data – “Read the digest now”. If she taps it, the bot receives a callback_query and can open the Mini App again (or just answer with a preview).
This pattern is elegant because the bot doesn’t need to create unique keyboards for each of its 100,000 users. It sends one keyboard to everyone. The personalization exists entirely in the app’s logic.
Security and Edge Cases
Validate Everything
If there’s one thing to remember: never trust the front end. Even though Telegram signs initData, an attacker can use a valid initData stolen from another user. That’s why you should always:
- Validate
initDataon a server, not just on the client. - Check
auth_dateand issue a timestamp error if the data is older than your allowed window. - Implement rate limiting on your API endpoints to prevent abuse.
Do Not Pass Secrets in URLs
The web_app URL is visible in network logs and can be extracted. If your app has secret parameters (like API keys), keep them out of the URL. Use the POST request flow instead.
Know Telegram’s Limits
Telegram Bot API places specific limits on different button fields. For example, callback_data has a hard limit of 64 bytes. For web_app URLs, there’s no documented public limit, but Telegram generally follows standard URL best practices. Keep URLs under ~2048 characters for compatibility.
Webhook or Polling?
For production bots, use webhooks rather than long polling. A webhook receives updates via a single HTTPS endpoint, which is both more reliable and easier to scale. When you use callbacks via the Bot API, your backend sends the update directly to Telegram's servers, and the webhook configuration is independent.
The Big Picture
The one inline button is more than a UI trick. It is a design philosophy: separate identity from interface. The button doesn’t need to know who pressed it; Telegram’s cryptographic handshake does the heavy lifting. Parameters shape the context, authentication establishes trust, and callbacks close the loop. This is how modern Telegram apps handle millions of users with one compact message.
If you’re building such an integration, choose tools that respect these principles. Some low-code platforms already simplify the auth flow: for instance, ASI Biont supports connection to Telegram through API – more on asibiont.com/courses. But even if you use custom code, the pattern is consistent.
Now go embed that button with confidence. One button. Everyone. All it takes is a clean parameter protocol, a cryptographic wrapper, and a callback that knows your name.
Comments