Telegram

Validate Telegram Mini App initData in Python: code and mistakes

Oct 30, 2026 · 4 min read

Summary

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:

  1. Parse the string into key-value pairs (values are URL-decoded).
  2. Remove the hash field, sort the remaining pairs by key and join them as key=value separated by a newline.
  3. Compute the secret key: HMAC-SHA256 where the key is the string WebAppData and the message is the bot token.
  4. Compute HMAC-SHA256 of the string from step 2 with that secret and compare the result (hex) with the received hash.
  5. Check that auth_date is 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 ==. Use hmac.compare_digest to 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. hash is 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.

Telegram Mini AppinitDataPythonsecurityHMAC

Plain version for AI and reading without styling: Markdown.

Read also

Tell me about your task

Describe what needs to be done, even in a couple of lines. I will reply with questions or an estimate.

Discuss a project →