# 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.

Date: 2026-10-30 · Source: https://oneswitch.ru/en/blog/proverka-initdata-telegram-mini-app-python/

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

```python
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:

```javascript
fetch("/api/me", { headers: { Authorization: "tma " + Telegram.WebApp.initData } });
```

The `tma` prefix is a common convention, not a Telegram requirement. An aiohttp example:

```python
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](/raboty/danang-house/) and [HotTrip](/raboty/hottrip/). If you are building a bot or a Mini App, see [Telegram bots and Mini Apps](/uslugi/telegram-boty-i-mini-app/).

## 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.
