Telegram Mini App with Vue and FastAPI: Validating initData
Connect a Vue 3 Telegram Mini App to FastAPI, send raw initData, validate its HMAC signature, check auth_date, and avoid Invalid signature errors.
Telegram Mini App with Vue and FastAPI
A Mini App can identify a Telegram user without a separate login form, but browser data is not automatically trustworthy. initDataUnsafe can be modified by the client. Telegram recommends sending the raw Telegram.WebApp.initData string to your backend and validating its signature there. The exact algorithm is documented in the official Mini Apps guide.
This article builds that flow with Vue 3 and FastAPI and explains the subtle double-decoding bug behind many Invalid signature errors.
Architecture
Telegram → Vue Mini App → X-Init-Data → FastAPI → HMAC validation → user
Vue only forwards Telegram's raw string. FastAPI validates the signature and age, then extracts the user. Profiles, orders, course access, and payments use that verified Telegram ID.
Load the Telegram SDK
Place the official SDK before your application bundle:
<script src="https://telegram.org/js/telegram-web-app.js"></script>
Then initialize the Mini App:
const tg = window.Telegram?.WebApp
tg?.ready()
tg?.expand()
Call ready() after applying the Telegram theme and preparing the first screen. This keeps the user from seeing an empty initial state.
Send raw initData from Vue
Centralize the header in your API client:
const API_URL = 'https://api.example.com/api/v1'
async function apiRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
const initData = window.Telegram?.WebApp?.initData ?? ''
const response = await fetch(`${API_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Init-Data': initData,
...options.headers,
},
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}
Do not send a separate authoritative user_id, username, or payment flag. The backend should use only the identity extracted from validated initData.
Validate the signature in Python
initData is a query string. Remove hash, sort the remaining pairs, and join them with line feeds.
import hashlib
import hmac
import json
import time
from urllib.parse import parse_qsl
class TelegramAuthError(ValueError):
pass
def validate_init_data(init_data: str, bot_token: str, max_age: int = 3600) -> dict:
values = dict(parse_qsl(init_data, keep_blank_values=True))
received_hash = values.pop("hash", None)
if not received_hash:
raise TelegramAuthError("Missing hash")
data_check_string = "\n".join(
f"{key}={value}" for key, value in sorted(values.items())
)
secret_key = hmac.new(
b"WebAppData",
bot_token.encode(),
hashlib.sha256,
).digest()
calculated_hash = hmac.new(
secret_key,
data_check_string.encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(calculated_hash, received_hash):
raise TelegramAuthError("Invalid signature")
auth_date = int(values.get("auth_date", "0"))
if auth_date <= 0 or time.time() - auth_date > max_age:
raise TelegramAuthError("initData expired")
return json.loads(values["user"])
hmac.compare_digest provides a safe comparison. The auth_date check limits replay of an old, otherwise valid string.
Why valid users get Invalid signature
The most common cause is modifying the string before validation:
# Do not do this
init_data = unquote(init_data)
parse_qsl already handles percent-encoding. An extra unquote() can decode characters inside the JSON twice and change the data-check string. The user is genuine, but the signature can no longer match.
Other common causes include:
- validating with a different bot token;
- removing another field from the data-check string;
- sorting after values have been transformed;
- using
initDataUnsafeinstead of the raw string; - changing spaces or
+characters; - rejecting a reasonable
auth_datebecause the allowed age is too short.
Use a FastAPI dependency
from fastapi import Header, HTTPException
async def telegram_user(x_init_data: str = Header(alias="X-Init-Data")) -> dict:
try:
return validate_init_data(x_init_data, settings.bot_token)
except TelegramAuthError as exc:
raise HTTPException(status_code=401, detail=str(exc)) from exc
Protect an endpoint with the dependency:
from fastapi import APIRouter, Depends
router = APIRouter()
@router.get("/profile")
async def profile(user: dict = Depends(telegram_user)):
return await profiles.get_or_create(telegram_id=user["id"])
The same dependency can protect profiles, courses, orders, and checkout endpoints.
Production checklist
- Launch the Mini App through a Telegram
web_appbutton. - Forward raw
initDatawithout manual decoding. - Validate with the token of the same bot.
- Enforce a reasonable
auth_dateage. - Never accept the authoritative Telegram ID in a JSON body.
- Serve the frontend and API over HTTPS.
- Provide a clear retry screen when authorization fails.
Use an explicit preview mode for local development instead of weakening the production check.
For a product overview, read what a Telegram Mini App is. The complete Telegram bot course covers Mini Apps, payments, testing, and deployment as one system.
Need a booking flow, store, or customer account inside Telegram? Describe the project.
share
Need a website or web app?
I build web services, dashboards and APIs with a modern stack.