The short answer
- 1User data a mini app receives cannot be trusted until its signature is checked.
- 2Send initData to your server, sort the pairs, compare the hash using a WebAppData key.
- 3Reject stale auth_date values and empty initData.
In this article
Why you must check it
When a mini app opens, Telegram passes the web page details about the user who opened it. That data lives in the browser on the user's device, so anyone who wants to can change it before it reaches your server. Your server should trust the user only after checking Telegram's signature, especially for anything involving money or accounts: awarding points, taking orders, logging in.
| Value | What it is | Use it for |
|---|---|---|
initData | The raw string, signature included | Send it to your server and verify it there |
initDataUnsafe | The same data already parsed into an object | Only for showing a name on screen; never trust it |
Verify it with your bot token
- The mini app sends the
Telegram.WebApp.initDatastring to your server unchanged. - Your server splits it into key=value pairs and sets
hashaside. - Sort the remaining pairs by key name and join them with newline characters. This is the data-check string.
- The secret key is the HMAC-SHA256 of your bot token, using the fixed key
WebAppData. - If the HMAC-SHA256 of the data-check string with that secret key, in hex, equals the
hashyou set aside, Telegram produced the data. - Reject it if
auth_date, the time it was created, is too old. This stops someone replaying data they stole earlier.
import hmac, hashlib, time
from urllib.parse import parse_qsl
def check(init_data, bot_token, max_age=3600):
pairs = dict(parse_qsl(init_data))
received = pairs.pop("hash", "")
text = chr(10).join(f"{k}={v}" for k, v in sorted(pairs.items())) # newline-joined
secret = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
ok = hmac.compare_digest(
hmac.new(secret, text.encode(), hashlib.sha256).hexdigest(), received)
return ok and time.time() - int(pairs.get("auth_date", 0)) < max_age
This is a minimal example using only Python's standard library. Choose max_age to suit your service.
Verifying without the token, for third parties
When a service other than your own server needs to confirm the user, initData also carries a signature field, an Ed25519 signature. It is checked with a public key that Telegram publishes, so you never hand over your bot token. The data-check string is built slightly differently, with the bot ID added at the start, so follow the official procedure exactly if you use this method.
Depending on how it opened, initData may be empty
| Where the mini app was opened from | initData |
|---|---|
| Inline button, menu button, main mini app, direct link, attachment menu | Present |
| Reply keyboard button, inline mode | Absent. An app opened from a reply keyboard returns up to 4096 bytes to the bot with sendData instead |
If initData is empty, treat the user as unknown. Letting an empty value through is the most common mistake.
To open a mini app from the menu button, see the bot menu button. To log users in on a website outside Telegram, see Log in with Telegram.
FAQ
How do I validate Telegram mini app initData?
On your server, remove hash, sort the other fields, join them with newlines, and compare the HMAC-SHA256 using a secret derived from your bot token with the key WebAppData.
Can I trust initDataUnsafe?
No. It is unverified data from the browser; use it only for display.
Why is initData empty in my mini app?
Mini apps opened from a reply keyboard button or inline mode do not receive initData.
Sources
At a glance
- 1Trust
initDataUnsafe is for display only.
- 2Check
HMAC-SHA256 with a key derived from the bot token; constant-time compare.
- 3Edge cases
Old auth_date and empty initData are rejected.
Updated