all articles
August 24, 2026·updated October 12, 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

By Shcoder, independent developer

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.

Important: initData depends on how the Mini App was launched. For authentication, use a launch method that supplies user data, such as a bot inline button (InlineKeyboardButton.web_app), the menu button, or the Main Mini App. A reply-keyboard KeyboardButton.web_app is intended for input interfaces and may not provide initData; it has a separate sendData() flow. Check the actual value of Telegram.WebApp.initData in each supported Telegram client before connecting it to your API. Launch methods and available data.

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 re
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:
    if not init_data or len(init_data) > 16_384:
        raise TelegramAuthError("Missing or oversized initData")

    try:
        pairs = parse_qsl(init_data, keep_blank_values=True, strict_parsing=True)
    except ValueError as exc:
        raise TelegramAuthError("Malformed initData") from exc
    if len({key for key, _ in pairs}) != len(pairs):
        raise TelegramAuthError("Duplicate initData field")

    values = dict(pairs)
    received_hash = values.pop("hash", None)
    if not received_hash or not re.fullmatch(r"[0-9a-f]{64}", 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")

    try:
        auth_date = int(values["auth_date"])
        user = json.loads(values["user"])
    except (KeyError, TypeError, ValueError) as exc:
        raise TelegramAuthError("Malformed auth_date or user") from exc
    now = int(time.time())
    if auth_date <= 0 or auth_date < now - max_age or auth_date > now + 30:
        raise TelegramAuthError("initData expired")
    if not isinstance(user, dict) or type(user.get("id")) is not int:
        raise TelegramAuthError("Missing Telegram user ID")

    return 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 using a method that supplies user initData (such as an InlineKeyboardButton.web_app, the menu button, or Main Mini App).
  • Confirm in real Telegram clients that Telegram.WebApp.initData is non-empty; KeyboardButton.web_app may use the separate sendData() flow.
  • 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.

Build a production Telegram bot end to end

The full course covers PostgreSQL, payments, Mini Apps, testing and Docker. Start with the free first lesson.

Open the preview lesson

Need a website or web app?

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