[{"data":1,"prerenderedAt":941},["ShallowReactive",2],{"blog-en-telegram-mini-app-vue-fastapi-initdata":3},{"id":4,"title":5,"body":6,"date":924,"description":925,"extension":926,"meta":927,"navigation":137,"path":928,"readTime":929,"seo":930,"slug":931,"stem":932,"tags":933,"updated":939,"__hash__":940},"blogEn\u002Fen\u002Fblog\u002Ftelegram-mini-app-vue-fastapi-initdata.md","Telegram Mini App with Vue and FastAPI: Validating initData",{"type":7,"value":8,"toc":915},"minimark",[9,14,35,42,47,57,60,64,67,106,109,161,168,172,175,440,450,454,463,683,693,697,700,715,725,728,760,764,807,810,848,851,855,888,891,904,911],[10,11,13],"h1",{"id":12},"telegram-mini-app-with-vue-and-fastapi","Telegram Mini App with Vue and FastAPI",[15,16,17,18,22,23,26,27,34],"p",{},"A Mini App can identify a Telegram user without a separate login form, but browser data is not automatically trustworthy. ",[19,20,21],"code",{},"initDataUnsafe"," can be modified by the client. Telegram recommends sending the raw ",[19,24,25],{},"Telegram.WebApp.initData"," string to your backend and validating its signature there. The exact algorithm is documented in the ",[28,29,33],"a",{"href":30,"rel":31},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fwebapps#validating-data-received-via-the-mini-app",[32],"nofollow","official Mini Apps guide",".",[15,36,37,38,41],{},"This article builds that flow with Vue 3 and FastAPI and explains the subtle double-decoding bug behind many ",[19,39,40],{},"Invalid signature"," errors.",[43,44,46],"h2",{"id":45},"architecture","Architecture",[48,49,55],"pre",{"className":50,"code":52,"language":53,"meta":54},[51],"language-text","Telegram → Vue Mini App → X-Init-Data → FastAPI → HMAC validation → user\n","text","",[19,56,52],{"__ignoreMap":54},[15,58,59],{},"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.",[43,61,63],{"id":62},"load-the-telegram-sdk","Load the Telegram SDK",[15,65,66],{},"Place the official SDK before your application bundle:",[48,68,72],{"className":69,"code":70,"language":71,"meta":54,"style":54},"language-html shiki shiki-themes github-light github-dark","\u003Cscript src=\"https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js\">\u003C\u002Fscript>\n","html",[19,73,74],{"__ignoreMap":54},[75,76,79,83,87,91,94,98,101,103],"span",{"class":77,"line":78},"line",1,[75,80,82],{"class":81},"sVt8B","\u003C",[75,84,86],{"class":85},"s9eBZ","script",[75,88,90],{"class":89},"sScJk"," src",[75,92,93],{"class":81},"=",[75,95,97],{"class":96},"sZZnC","\"https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js\"",[75,99,100],{"class":81},">\u003C\u002F",[75,102,86],{"class":85},[75,104,105],{"class":81},">\n",[15,107,108],{},"Then initialize the Mini App:",[48,110,114],{"className":111,"code":112,"language":113,"meta":54,"style":54},"language-ts shiki shiki-themes github-light github-dark","const tg = window.Telegram?.WebApp\n\ntg?.ready()\ntg?.expand()\n","ts",[19,115,116,132,139,151],{"__ignoreMap":54},[75,117,118,122,126,129],{"class":77,"line":78},[75,119,121],{"class":120},"szBVR","const",[75,123,125],{"class":124},"sj4cs"," tg",[75,127,128],{"class":120}," =",[75,130,131],{"class":81}," window.Telegram?.WebApp\n",[75,133,135],{"class":77,"line":134},2,[75,136,138],{"emptyLinePlaceholder":137},true,"\n",[75,140,142,145,148],{"class":77,"line":141},3,[75,143,144],{"class":81},"tg?.",[75,146,147],{"class":89},"ready",[75,149,150],{"class":81},"()\n",[75,152,154,156,159],{"class":77,"line":153},4,[75,155,144],{"class":81},[75,157,158],{"class":89},"expand",[75,160,150],{"class":81},[15,162,163,164,167],{},"Call ",[19,165,166],{},"ready()"," after applying the Telegram theme and preparing the first screen. This keeps the user from seeing an empty initial state.",[43,169,171],{"id":170},"send-raw-initdata-from-vue","Send raw initData from Vue",[15,173,174],{},"Centralize the header in your API client:",[48,176,178],{"className":111,"code":177,"language":113,"meta":54,"style":54},"const API_URL = 'https:\u002F\u002Fapi.example.com\u002Fapi\u002Fv1'\n\nasync function apiRequest\u003CT>(path: string, options: RequestInit = {}): Promise\u003CT> {\n  const initData = window.Telegram?.WebApp?.initData ?? ''\n\n  const response = await fetch(`${API_URL}${path}`, {\n    ...options,\n    headers: {\n      'Content-Type': 'application\u002Fjson',\n      'X-Init-Data': initData,\n      ...options.headers,\n    },\n  })\n\n  if (!response.ok) throw new Error(`HTTP ${response.status}`)\n  return response.json()\n}\n",[19,179,180,192,196,253,272,277,313,322,328,343,352,361,367,373,378,420,434],{"__ignoreMap":54},[75,181,182,184,187,189],{"class":77,"line":78},[75,183,121],{"class":120},[75,185,186],{"class":124}," API_URL",[75,188,128],{"class":120},[75,190,191],{"class":96}," 'https:\u002F\u002Fapi.example.com\u002Fapi\u002Fv1'\n",[75,193,194],{"class":77,"line":134},[75,195,138],{"emptyLinePlaceholder":137},[75,197,198,201,204,207,209,212,215,219,222,225,228,231,233,236,238,241,243,246,248,250],{"class":77,"line":141},[75,199,200],{"class":120},"async",[75,202,203],{"class":120}," function",[75,205,206],{"class":89}," apiRequest",[75,208,82],{"class":81},[75,210,211],{"class":89},"T",[75,213,214],{"class":81},">(",[75,216,218],{"class":217},"s4XuR","path",[75,220,221],{"class":120},":",[75,223,224],{"class":124}," string",[75,226,227],{"class":81},", ",[75,229,230],{"class":217},"options",[75,232,221],{"class":120},[75,234,235],{"class":89}," RequestInit",[75,237,128],{"class":120},[75,239,240],{"class":81}," {})",[75,242,221],{"class":120},[75,244,245],{"class":89}," Promise",[75,247,82],{"class":81},[75,249,211],{"class":89},[75,251,252],{"class":81},"> {\n",[75,254,255,258,261,263,266,269],{"class":77,"line":153},[75,256,257],{"class":120},"  const",[75,259,260],{"class":124}," initData",[75,262,128],{"class":120},[75,264,265],{"class":81}," window.Telegram?.WebApp?.initData ",[75,267,268],{"class":120},"??",[75,270,271],{"class":96}," ''\n",[75,273,275],{"class":77,"line":274},5,[75,276,138],{"emptyLinePlaceholder":137},[75,278,280,282,285,287,290,293,296,299,302,305,307,310],{"class":77,"line":279},6,[75,281,257],{"class":120},[75,283,284],{"class":124}," response",[75,286,128],{"class":120},[75,288,289],{"class":120}," await",[75,291,292],{"class":89}," fetch",[75,294,295],{"class":81},"(",[75,297,298],{"class":96},"`${",[75,300,301],{"class":124},"API_URL",[75,303,304],{"class":96},"}${",[75,306,218],{"class":81},[75,308,309],{"class":96},"}`",[75,311,312],{"class":81},", {\n",[75,314,316,319],{"class":77,"line":315},7,[75,317,318],{"class":120},"    ...",[75,320,321],{"class":81},"options,\n",[75,323,325],{"class":77,"line":324},8,[75,326,327],{"class":81},"    headers: {\n",[75,329,331,334,337,340],{"class":77,"line":330},9,[75,332,333],{"class":96},"      'Content-Type'",[75,335,336],{"class":81},": ",[75,338,339],{"class":96},"'application\u002Fjson'",[75,341,342],{"class":81},",\n",[75,344,346,349],{"class":77,"line":345},10,[75,347,348],{"class":96},"      'X-Init-Data'",[75,350,351],{"class":81},": initData,\n",[75,353,355,358],{"class":77,"line":354},11,[75,356,357],{"class":120},"      ...",[75,359,360],{"class":81},"options.headers,\n",[75,362,364],{"class":77,"line":363},12,[75,365,366],{"class":81},"    },\n",[75,368,370],{"class":77,"line":369},13,[75,371,372],{"class":81},"  })\n",[75,374,376],{"class":77,"line":375},14,[75,377,138],{"emptyLinePlaceholder":137},[75,379,381,384,387,390,393,396,399,402,404,407,410,412,415,417],{"class":77,"line":380},15,[75,382,383],{"class":120},"  if",[75,385,386],{"class":81}," (",[75,388,389],{"class":120},"!",[75,391,392],{"class":81},"response.ok) ",[75,394,395],{"class":120},"throw",[75,397,398],{"class":120}," new",[75,400,401],{"class":89}," Error",[75,403,295],{"class":81},[75,405,406],{"class":96},"`HTTP ${",[75,408,409],{"class":81},"response",[75,411,34],{"class":96},[75,413,414],{"class":81},"status",[75,416,309],{"class":96},[75,418,419],{"class":81},")\n",[75,421,423,426,429,432],{"class":77,"line":422},16,[75,424,425],{"class":120},"  return",[75,427,428],{"class":81}," response.",[75,430,431],{"class":89},"json",[75,433,150],{"class":81},[75,435,437],{"class":77,"line":436},17,[75,438,439],{"class":81},"}\n",[15,441,442,443,446,447,34],{},"Do not send a separate authoritative ",[19,444,445],{},"user_id",", username, or payment flag. The backend should use only the identity extracted from validated ",[19,448,449],{},"initData",[43,451,453],{"id":452},"validate-the-signature-in-python","Validate the signature in Python",[15,455,456,458,459,462],{},[19,457,449],{}," is a query string. Remove ",[19,460,461],{},"hash",", sort the remaining pairs, and join them with line feeds.",[48,464,468],{"className":465,"code":466,"language":467,"meta":54,"style":54},"language-python shiki shiki-themes github-light github-dark","import hashlib\nimport hmac\nimport json\nimport time\nfrom urllib.parse import parse_qsl\n\n\nclass TelegramAuthError(ValueError):\n    pass\n\n\ndef validate_init_data(init_data: str, bot_token: str, max_age: int = 3600) -> dict:\n    values = dict(parse_qsl(init_data, keep_blank_values=True))\n    received_hash = values.pop(\"hash\", None)\n    if not received_hash:\n        raise TelegramAuthError(\"Missing hash\")\n\n    data_check_string = \"\\n\".join(\n        f\"{key}={value}\" for key, value in sorted(values.items())\n    )\n\n    secret_key = hmac.new(\n        b\"WebAppData\",\n        bot_token.encode(),\n        hashlib.sha256,\n    ).digest()\n    calculated_hash = hmac.new(\n        secret_key,\n        data_check_string.encode(),\n        hashlib.sha256,\n    ).hexdigest()\n\n    if not hmac.compare_digest(calculated_hash, received_hash):\n        raise TelegramAuthError(\"Invalid signature\")\n\n    auth_date = int(values.get(\"auth_date\", \"0\"))\n    if auth_date \u003C= 0 or time.time() - auth_date > max_age:\n        raise TelegramAuthError(\"initData expired\")\n\n    return json.loads(values[\"user\"])\n","python",[19,469,470,475,480,485,490,495,499,503,508,513,517,521,526,531,536,541,546,550,556,562,568,573,579,585,591,597,603,609,615,621,626,632,637,643,649,654,660,666,672,677],{"__ignoreMap":54},[75,471,472],{"class":77,"line":78},[75,473,474],{},"import hashlib\n",[75,476,477],{"class":77,"line":134},[75,478,479],{},"import hmac\n",[75,481,482],{"class":77,"line":141},[75,483,484],{},"import json\n",[75,486,487],{"class":77,"line":153},[75,488,489],{},"import time\n",[75,491,492],{"class":77,"line":274},[75,493,494],{},"from urllib.parse import parse_qsl\n",[75,496,497],{"class":77,"line":279},[75,498,138],{"emptyLinePlaceholder":137},[75,500,501],{"class":77,"line":315},[75,502,138],{"emptyLinePlaceholder":137},[75,504,505],{"class":77,"line":324},[75,506,507],{},"class TelegramAuthError(ValueError):\n",[75,509,510],{"class":77,"line":330},[75,511,512],{},"    pass\n",[75,514,515],{"class":77,"line":345},[75,516,138],{"emptyLinePlaceholder":137},[75,518,519],{"class":77,"line":354},[75,520,138],{"emptyLinePlaceholder":137},[75,522,523],{"class":77,"line":363},[75,524,525],{},"def validate_init_data(init_data: str, bot_token: str, max_age: int = 3600) -> dict:\n",[75,527,528],{"class":77,"line":369},[75,529,530],{},"    values = dict(parse_qsl(init_data, keep_blank_values=True))\n",[75,532,533],{"class":77,"line":375},[75,534,535],{},"    received_hash = values.pop(\"hash\", None)\n",[75,537,538],{"class":77,"line":380},[75,539,540],{},"    if not received_hash:\n",[75,542,543],{"class":77,"line":422},[75,544,545],{},"        raise TelegramAuthError(\"Missing hash\")\n",[75,547,548],{"class":77,"line":436},[75,549,138],{"emptyLinePlaceholder":137},[75,551,553],{"class":77,"line":552},18,[75,554,555],{},"    data_check_string = \"\\n\".join(\n",[75,557,559],{"class":77,"line":558},19,[75,560,561],{},"        f\"{key}={value}\" for key, value in sorted(values.items())\n",[75,563,565],{"class":77,"line":564},20,[75,566,567],{},"    )\n",[75,569,571],{"class":77,"line":570},21,[75,572,138],{"emptyLinePlaceholder":137},[75,574,576],{"class":77,"line":575},22,[75,577,578],{},"    secret_key = hmac.new(\n",[75,580,582],{"class":77,"line":581},23,[75,583,584],{},"        b\"WebAppData\",\n",[75,586,588],{"class":77,"line":587},24,[75,589,590],{},"        bot_token.encode(),\n",[75,592,594],{"class":77,"line":593},25,[75,595,596],{},"        hashlib.sha256,\n",[75,598,600],{"class":77,"line":599},26,[75,601,602],{},"    ).digest()\n",[75,604,606],{"class":77,"line":605},27,[75,607,608],{},"    calculated_hash = hmac.new(\n",[75,610,612],{"class":77,"line":611},28,[75,613,614],{},"        secret_key,\n",[75,616,618],{"class":77,"line":617},29,[75,619,620],{},"        data_check_string.encode(),\n",[75,622,624],{"class":77,"line":623},30,[75,625,596],{},[75,627,629],{"class":77,"line":628},31,[75,630,631],{},"    ).hexdigest()\n",[75,633,635],{"class":77,"line":634},32,[75,636,138],{"emptyLinePlaceholder":137},[75,638,640],{"class":77,"line":639},33,[75,641,642],{},"    if not hmac.compare_digest(calculated_hash, received_hash):\n",[75,644,646],{"class":77,"line":645},34,[75,647,648],{},"        raise TelegramAuthError(\"Invalid signature\")\n",[75,650,652],{"class":77,"line":651},35,[75,653,138],{"emptyLinePlaceholder":137},[75,655,657],{"class":77,"line":656},36,[75,658,659],{},"    auth_date = int(values.get(\"auth_date\", \"0\"))\n",[75,661,663],{"class":77,"line":662},37,[75,664,665],{},"    if auth_date \u003C= 0 or time.time() - auth_date > max_age:\n",[75,667,669],{"class":77,"line":668},38,[75,670,671],{},"        raise TelegramAuthError(\"initData expired\")\n",[75,673,675],{"class":77,"line":674},39,[75,676,138],{"emptyLinePlaceholder":137},[75,678,680],{"class":77,"line":679},40,[75,681,682],{},"    return json.loads(values[\"user\"])\n",[15,684,685,688,689,692],{},[19,686,687],{},"hmac.compare_digest"," provides a safe comparison. The ",[19,690,691],{},"auth_date"," check limits replay of an old, otherwise valid string.",[43,694,696],{"id":695},"why-valid-users-get-invalid-signature","Why valid users get Invalid signature",[15,698,699],{},"The most common cause is modifying the string before validation:",[48,701,703],{"className":465,"code":702,"language":467,"meta":54,"style":54},"# Do not do this\ninit_data = unquote(init_data)\n",[19,704,705,710],{"__ignoreMap":54},[75,706,707],{"class":77,"line":78},[75,708,709],{},"# Do not do this\n",[75,711,712],{"class":77,"line":134},[75,713,714],{},"init_data = unquote(init_data)\n",[15,716,717,720,721,724],{},[19,718,719],{},"parse_qsl"," already handles percent-encoding. An extra ",[19,722,723],{},"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.",[15,726,727],{},"Other common causes include:",[729,730,731,735,738,741,747,754],"ul",{},[732,733,734],"li",{},"validating with a different bot token;",[732,736,737],{},"removing another field from the data-check string;",[732,739,740],{},"sorting after values have been transformed;",[732,742,743,744,746],{},"using ",[19,745,21],{}," instead of the raw string;",[732,748,749,750,753],{},"changing spaces or ",[19,751,752],{},"+"," characters;",[732,755,756,757,759],{},"rejecting a reasonable ",[19,758,691],{}," because the allowed age is too short.",[43,761,763],{"id":762},"use-a-fastapi-dependency","Use a FastAPI dependency",[48,765,767],{"className":465,"code":766,"language":467,"meta":54,"style":54},"from fastapi import Header, HTTPException\n\n\nasync def telegram_user(x_init_data: str = Header(alias=\"X-Init-Data\")) -> dict:\n    try:\n        return validate_init_data(x_init_data, settings.bot_token)\n    except TelegramAuthError as exc:\n        raise HTTPException(status_code=401, detail=str(exc)) from exc\n",[19,768,769,774,778,782,787,792,797,802],{"__ignoreMap":54},[75,770,771],{"class":77,"line":78},[75,772,773],{},"from fastapi import Header, HTTPException\n",[75,775,776],{"class":77,"line":134},[75,777,138],{"emptyLinePlaceholder":137},[75,779,780],{"class":77,"line":141},[75,781,138],{"emptyLinePlaceholder":137},[75,783,784],{"class":77,"line":153},[75,785,786],{},"async def telegram_user(x_init_data: str = Header(alias=\"X-Init-Data\")) -> dict:\n",[75,788,789],{"class":77,"line":274},[75,790,791],{},"    try:\n",[75,793,794],{"class":77,"line":279},[75,795,796],{},"        return validate_init_data(x_init_data, settings.bot_token)\n",[75,798,799],{"class":77,"line":315},[75,800,801],{},"    except TelegramAuthError as exc:\n",[75,803,804],{"class":77,"line":324},[75,805,806],{},"        raise HTTPException(status_code=401, detail=str(exc)) from exc\n",[15,808,809],{},"Protect an endpoint with the dependency:",[48,811,813],{"className":465,"code":812,"language":467,"meta":54,"style":54},"from fastapi import APIRouter, Depends\n\nrouter = APIRouter()\n\n@router.get(\"\u002Fprofile\")\nasync def profile(user: dict = Depends(telegram_user)):\n    return await profiles.get_or_create(telegram_id=user[\"id\"])\n",[19,814,815,820,824,829,833,838,843],{"__ignoreMap":54},[75,816,817],{"class":77,"line":78},[75,818,819],{},"from fastapi import APIRouter, Depends\n",[75,821,822],{"class":77,"line":134},[75,823,138],{"emptyLinePlaceholder":137},[75,825,826],{"class":77,"line":141},[75,827,828],{},"router = APIRouter()\n",[75,830,831],{"class":77,"line":153},[75,832,138],{"emptyLinePlaceholder":137},[75,834,835],{"class":77,"line":274},[75,836,837],{},"@router.get(\"\u002Fprofile\")\n",[75,839,840],{"class":77,"line":279},[75,841,842],{},"async def profile(user: dict = Depends(telegram_user)):\n",[75,844,845],{"class":77,"line":315},[75,846,847],{},"    return await profiles.get_or_create(telegram_id=user[\"id\"])\n",[15,849,850],{},"The same dependency can protect profiles, courses, orders, and checkout endpoints.",[43,852,854],{"id":853},"production-checklist","Production checklist",[729,856,857,864,870,873,879,882,885],{},[732,858,859,860,863],{},"Launch the Mini App through a Telegram ",[19,861,862],{},"web_app"," button.",[732,865,866,867,869],{},"Forward raw ",[19,868,449],{}," without manual decoding.",[732,871,872],{},"Validate with the token of the same bot.",[732,874,875,876,878],{},"Enforce a reasonable ",[19,877,691],{}," age.",[732,880,881],{},"Never accept the authoritative Telegram ID in a JSON body.",[732,883,884],{},"Serve the frontend and API over HTTPS.",[732,886,887],{},"Provide a clear retry screen when authorization fails.",[15,889,890],{},"Use an explicit preview mode for local development instead of weakening the production check.",[15,892,893,894,898,899,903],{},"For a product overview, read ",[28,895,897],{"href":896},"\u002Fen\u002Fblog\u002Ftelegram-mini-app-guide\u002F","what a Telegram Mini App is",". The ",[28,900,902],{"href":901},"\u002Fen\u002Fcourses\u002Ftelegram-bots-python\u002F","complete Telegram bot course"," covers Mini Apps, payments, testing, and deployment as one system.",[15,905,906,907,34],{},"Need a booking flow, store, or customer account inside Telegram? ",[28,908,910],{"href":909},"\u002Fen\u002F#contact","Describe the project",[912,913,914],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":54,"searchDepth":134,"depth":134,"links":916},[917,918,919,920,921,922,923],{"id":45,"depth":134,"text":46},{"id":62,"depth":134,"text":63},{"id":170,"depth":134,"text":171},{"id":452,"depth":134,"text":453},{"id":695,"depth":134,"text":696},{"id":762,"depth":134,"text":763},{"id":853,"depth":134,"text":854},"2026-08-24","Connect a Vue 3 Telegram Mini App to FastAPI, send raw initData, validate its HMAC signature, check auth_date, and avoid Invalid signature errors.","md",{},"\u002Fen\u002Fblog\u002Ftelegram-mini-app-vue-fastapi-initdata","12 min",{"title":5,"description":925},"telegram-mini-app-vue-fastapi-initdata","en\u002Fblog\u002Ftelegram-mini-app-vue-fastapi-initdata",[934,935,936,937,938],"Telegram Mini App","Vue.js","FastAPI","Python","Security",null,"ls6XP1Ab-0GrA5JNlpFMvQn2BFPFH47uJ1nBHvCNDsg",1787594770987]