Validate Telegram Mini App initData in Python: code and mistakes
Validate initData on the server: derive a secret as HMAC-SHA256 of the bot token with the key WebAppData, then compare the signature of the data-check string. Use compare_digest and check auth_date to reject forged and stale data.
When a Mini App calls your server, you need to be sure the request really came from Telegram for a specific user, and not from an attacker who substituted someone else's user_id. For this Telegram signs the initData string and your server verifies the signature.
How validation works
A Mini App receives Telegram.WebApp.initData, a string like query_id=...&user=...&auth_date=...&hash=.... Validation goes like this:
- Parse the string into key-value pairs (values are URL-decoded).
- Remove the
hashfield, sort the remaining pairs by key and join them askey=valueseparated by a newline. - Compute the secret key: HMAC-SHA256 where the key is the string
WebAppDataand the message is the bot token. - Compute HMAC-SHA256 of the string from step 2 with that secret and compare the result (hex) with the received
hash. - Check that
auth_dateis not older than a reasonable limit.
Working code
import hashlib
import hmac
import json
import time
from urllib.parse import parse_qsl
def validate_init_data(init_data: str, bot_token: str, max_age: int = 3600) -> dict:
"""Returns the Mini App data if the signature is valid and fresh, otherwise raises ValueError."""
pairs = dict(parse_qsl(init_data, keep_blank_values=True))
received_hash = pairs.pop("hash", None)
if not received_hash:
raise ValueError("missing hash")
check_string = "\n".join(f"{k}={v}" for k, v in sorted(pairs.items()))
secret_key = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
expected = hmac.new(secret_key, check_string.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, received_hash):
raise ValueError("signature mismatch")
if time.time() - int(pairs.get("auth_date", 0)) > max_age:
raise ValueError("data expired")
if "user" in pairs:
pairs["user"] = json.loads(pairs["user"])
return pairs
We ran this code on a signed sample, on tampered data, on a wrong token and on a stale auth_date: valid data is accepted and the rest is rejected.
How to send initData to the server
The frontend sends the string in a request header and the server validates it on every call:
fetch("/api/me", { headers: { Authorization: "tma " + Telegram.WebApp.initData } });
The tma prefix is a common convention, not a Telegram requirement. An aiohttp example:
from aiohttp import web
async def me(request: web.Request) -> web.Response:
auth = request.headers.get("Authorization", "")
if not auth.startswith("tma "):
raise web.HTTPUnauthorized()
try:
data = validate_init_data(auth[4:], BOT_TOKEN)
except ValueError:
raise web.HTTPUnauthorized()
return web.json_response({"user_id": data["user"]["id"]})
In FastAPI the same function plugs in as a dependency (Depends) that reads the header and calls validate_init_data.
Common mistakes
- Validating on the client. The signature must be checked on the server only: the bot token must never reach the frontend.
- Comparing with
==. Usehmac.compare_digestto avoid a timing attack. - Forgetting
auth_date. Without an expiry check, intercepted old data works forever. - Wrong key order or extra fields in the check string.
hashis not part of the string; the rest are sorted alphabetically. - Wrong token. The signature is tied to a specific bot's token. With several bots, store the token with the app.
- Trusting
initDataUnsafe. It is the same data without validation. Never use it for authorization.
Where it is useful
Validating initData is needed by any Mini App that stores user data: favorites, notification subscriptions, saved searches. That is how the Mini Apps in our projects work: Da Nang House and HotTrip. If you are building a bot or a Mini App, see Telegram bots and Mini Apps.
FAQ
Can I trust user_id from initDataUnsafe?
No. `initDataUnsafe` is parsed data without signature validation. Use only data that passed `validate_init_data` for authorization.
How long is initData valid?
Telegram passes the creation time in `auth_date`, and you decide the lifetime. An hour is enough for ordinary requests; sensitive operations use less.
What should I do on a “signature mismatch” error?
Check that the token belongs to the bot that opened the Mini App, that `hash` is excluded from the check string, and that pairs are sorted by key and joined with a newline.
Do I need validation if the Mini App only reads public data?
If the data is public and not tied to a user, validation is optional. As soon as personal data or actions on behalf of a user appear, it is required.
Plain version for AI and reading without styling: Markdown.