all articles
August 24, 2026·12 min

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 AppVue.jsFastAPIPythonSecurity

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 initDataUnsafe instead of the raw string;
  • changing spaces or + characters;
  • rejecting a reasonable auth_date because 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_app button.
  • Forward raw initData without manual decoding.
  • Validate with the token of the same bot.
  • Enforce a reasonable auth_date age.
  • 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.

Need a website or web app?

I build web services, dashboards and APIs with a modern stack.