[{"data":1,"prerenderedAt":957},["ShallowReactive",2],{"blog-ru-telegram-mini-app-vue-fastapi-initdata":3},{"id":4,"title":5,"body":6,"date":940,"description":941,"extension":942,"meta":943,"navigation":143,"path":944,"readTime":945,"seo":946,"slug":947,"stem":948,"tags":949,"updated":955,"__hash__":956},"blogRu\u002Fru\u002Fblog\u002Ftelegram-mini-app-vue-fastapi-initdata.md","Telegram Mini App на Vue и FastAPI: initData и авторизация",{"type":7,"value":8,"toc":930},"minimark",[9,14,35,42,47,50,60,63,67,70,109,115,167,173,177,180,445,455,459,469,689,699,703,706,721,735,738,771,775,778,821,824,862,865,869,900,903,907,919,926],[10,11,13],"h1",{"id":12},"telegram-mini-app-на-vue-и-fastapi-безопасная-авторизация","Telegram Mini App на Vue и FastAPI: безопасная авторизация",[15,16,17,18,22,23,26,27,34],"p",{},"Mini App получает Telegram-пользователя без отдельной формы входа. Но данные из браузера нельзя считать доверенными: ",[19,20,21],"code",{},"initDataUnsafe"," легко подменить вручную. Telegram рекомендует отправлять на backend исходную строку ",[19,24,25],{},"Telegram.WebApp.initData"," и проверять её подпись на сервере. Алгоритм описан в ",[28,29,33],"a",{"href":30,"rel":31},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fwebapps#validating-data-received-via-the-mini-app",[32],"nofollow","официальной документации Mini Apps",".",[15,36,37,38,41],{},"Разберём рабочую архитектуру на Vue 3 и FastAPI, включая ошибку ",[19,39,40],{},"Invalid signature",", которая часто появляется из-за лишнего декодирования строки.",[43,44,46],"h2",{"id":45},"архитектура","Архитектура",[15,48,49],{},"Поток запроса выглядит так:",[51,52,58],"pre",{"className":53,"code":55,"language":56,"meta":57},[54],"language-text","Telegram → Vue Mini App → X-Init-Data → FastAPI → проверка HMAC → пользователь\n","text","",[19,59,55],{"__ignoreMap":57},[15,61,62],{},"Vue не решает, кто авторизован. Он только прикладывает исходную строку к каждому API-запросу. FastAPI проверяет подпись, срок действия и извлекает пользователя.",[43,64,66],{"id":65},"подключение-telegram-sdk","Подключение Telegram SDK",[15,68,69],{},"Добавьте официальный SDK до основного JavaScript-бандла:",[51,71,75],{"className":72,"code":73,"language":74,"meta":57,"style":57},"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,76,77],{"__ignoreMap":57},[78,79,82,86,90,94,97,101,104,106],"span",{"class":80,"line":81},"line",1,[78,83,85],{"class":84},"sVt8B","\u003C",[78,87,89],{"class":88},"s9eBZ","script",[78,91,93],{"class":92},"sScJk"," src",[78,95,96],{"class":84},"=",[78,98,100],{"class":99},"sZZnC","\"https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js\"",[78,102,103],{"class":84},">\u003C\u002F",[78,105,89],{"class":88},[78,107,108],{"class":84},">\n",[15,110,111,112,34],{},"После этого доступен объект ",[19,113,114],{},"window.Telegram.WebApp",[51,116,120],{"className":117,"code":118,"language":119,"meta":57,"style":57},"language-ts shiki shiki-themes github-light github-dark","const tg = window.Telegram?.WebApp\n\ntg?.ready()\ntg?.expand()\n","ts",[19,121,122,138,145,157],{"__ignoreMap":57},[78,123,124,128,132,135],{"class":80,"line":81},[78,125,127],{"class":126},"szBVR","const",[78,129,131],{"class":130},"sj4cs"," tg",[78,133,134],{"class":126}," =",[78,136,137],{"class":84}," window.Telegram?.WebApp\n",[78,139,141],{"class":80,"line":140},2,[78,142,144],{"emptyLinePlaceholder":143},true,"\n",[78,146,148,151,154],{"class":80,"line":147},3,[78,149,150],{"class":84},"tg?.",[78,152,153],{"class":92},"ready",[78,155,156],{"class":84},"()\n",[78,158,160,162,165],{"class":80,"line":159},4,[78,161,150],{"class":84},[78,163,164],{"class":92},"expand",[78,166,156],{"class":84},[15,168,169,172],{},[19,170,171],{},"ready()"," сообщает Telegram, что интерфейс можно показать. Его стоит вызывать после применения темы и подготовки первого экрана, чтобы пользователь не видел пустое состояние.",[43,174,176],{"id":175},"передача-initdata-из-vue","Передача initData из Vue",[15,178,179],{},"Удобнее централизовать заголовок в API-клиенте:",[51,181,183],{"className":117,"code":182,"language":119,"meta":57,"style":57},"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,184,185,197,201,258,277,282,318,327,333,348,357,366,372,378,383,425,439],{"__ignoreMap":57},[78,186,187,189,192,194],{"class":80,"line":81},[78,188,127],{"class":126},[78,190,191],{"class":130}," API_URL",[78,193,134],{"class":126},[78,195,196],{"class":99}," 'https:\u002F\u002Fapi.example.com\u002Fapi\u002Fv1'\n",[78,198,199],{"class":80,"line":140},[78,200,144],{"emptyLinePlaceholder":143},[78,202,203,206,209,212,214,217,220,224,227,230,233,236,238,241,243,246,248,251,253,255],{"class":80,"line":147},[78,204,205],{"class":126},"async",[78,207,208],{"class":126}," function",[78,210,211],{"class":92}," apiRequest",[78,213,85],{"class":84},[78,215,216],{"class":92},"T",[78,218,219],{"class":84},">(",[78,221,223],{"class":222},"s4XuR","path",[78,225,226],{"class":126},":",[78,228,229],{"class":130}," string",[78,231,232],{"class":84},", ",[78,234,235],{"class":222},"options",[78,237,226],{"class":126},[78,239,240],{"class":92}," RequestInit",[78,242,134],{"class":126},[78,244,245],{"class":84}," {})",[78,247,226],{"class":126},[78,249,250],{"class":92}," Promise",[78,252,85],{"class":84},[78,254,216],{"class":92},[78,256,257],{"class":84},"> {\n",[78,259,260,263,266,268,271,274],{"class":80,"line":159},[78,261,262],{"class":126},"  const",[78,264,265],{"class":130}," initData",[78,267,134],{"class":126},[78,269,270],{"class":84}," window.Telegram?.WebApp?.initData ",[78,272,273],{"class":126},"??",[78,275,276],{"class":99}," ''\n",[78,278,280],{"class":80,"line":279},5,[78,281,144],{"emptyLinePlaceholder":143},[78,283,285,287,290,292,295,298,301,304,307,310,312,315],{"class":80,"line":284},6,[78,286,262],{"class":126},[78,288,289],{"class":130}," response",[78,291,134],{"class":126},[78,293,294],{"class":126}," await",[78,296,297],{"class":92}," fetch",[78,299,300],{"class":84},"(",[78,302,303],{"class":99},"`${",[78,305,306],{"class":130},"API_URL",[78,308,309],{"class":99},"}${",[78,311,223],{"class":84},[78,313,314],{"class":99},"}`",[78,316,317],{"class":84},", {\n",[78,319,321,324],{"class":80,"line":320},7,[78,322,323],{"class":126},"    ...",[78,325,326],{"class":84},"options,\n",[78,328,330],{"class":80,"line":329},8,[78,331,332],{"class":84},"    headers: {\n",[78,334,336,339,342,345],{"class":80,"line":335},9,[78,337,338],{"class":99},"      'Content-Type'",[78,340,341],{"class":84},": ",[78,343,344],{"class":99},"'application\u002Fjson'",[78,346,347],{"class":84},",\n",[78,349,351,354],{"class":80,"line":350},10,[78,352,353],{"class":99},"      'X-Init-Data'",[78,355,356],{"class":84},": initData,\n",[78,358,360,363],{"class":80,"line":359},11,[78,361,362],{"class":126},"      ...",[78,364,365],{"class":84},"options.headers,\n",[78,367,369],{"class":80,"line":368},12,[78,370,371],{"class":84},"    },\n",[78,373,375],{"class":80,"line":374},13,[78,376,377],{"class":84},"  })\n",[78,379,381],{"class":80,"line":380},14,[78,382,144],{"emptyLinePlaceholder":143},[78,384,386,389,392,395,398,401,404,407,409,412,415,417,420,422],{"class":80,"line":385},15,[78,387,388],{"class":126},"  if",[78,390,391],{"class":84}," (",[78,393,394],{"class":126},"!",[78,396,397],{"class":84},"response.ok) ",[78,399,400],{"class":126},"throw",[78,402,403],{"class":126}," new",[78,405,406],{"class":92}," Error",[78,408,300],{"class":84},[78,410,411],{"class":99},"`HTTP ${",[78,413,414],{"class":84},"response",[78,416,34],{"class":99},[78,418,419],{"class":84},"status",[78,421,314],{"class":99},[78,423,424],{"class":84},")\n",[78,426,428,431,434,437],{"class":80,"line":427},16,[78,429,430],{"class":126},"  return",[78,432,433],{"class":84}," response.",[78,435,436],{"class":92},"json",[78,438,156],{"class":84},[78,440,442],{"class":80,"line":441},17,[78,443,444],{"class":84},"}\n",[15,446,447,448,451,452,34],{},"Не отправляйте отдельно ",[19,449,450],{},"user_id",", username или факт оплаты. Backend должен брать Telegram ID только из проверенного ",[19,453,454],{},"initData",[43,456,458],{"id":457},"проверка-подписи-в-python","Проверка подписи в Python",[15,460,461,462,464,465,468],{},"Строка ",[19,463,454],{}," имеет формат query string. Нужно убрать ",[19,466,467],{},"hash",", отсортировать остальные пары и собрать их через перенос строки.",[51,470,474],{"className":471,"code":472,"language":473,"meta":57,"style":57},"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,475,476,481,486,491,496,501,505,509,514,519,523,527,532,537,542,547,552,556,562,568,574,579,585,591,597,603,609,615,621,627,632,638,643,649,655,660,666,672,678,683],{"__ignoreMap":57},[78,477,478],{"class":80,"line":81},[78,479,480],{},"import hashlib\n",[78,482,483],{"class":80,"line":140},[78,484,485],{},"import hmac\n",[78,487,488],{"class":80,"line":147},[78,489,490],{},"import json\n",[78,492,493],{"class":80,"line":159},[78,494,495],{},"import time\n",[78,497,498],{"class":80,"line":279},[78,499,500],{},"from urllib.parse import parse_qsl\n",[78,502,503],{"class":80,"line":284},[78,504,144],{"emptyLinePlaceholder":143},[78,506,507],{"class":80,"line":320},[78,508,144],{"emptyLinePlaceholder":143},[78,510,511],{"class":80,"line":329},[78,512,513],{},"class TelegramAuthError(ValueError):\n",[78,515,516],{"class":80,"line":335},[78,517,518],{},"    pass\n",[78,520,521],{"class":80,"line":350},[78,522,144],{"emptyLinePlaceholder":143},[78,524,525],{"class":80,"line":359},[78,526,144],{"emptyLinePlaceholder":143},[78,528,529],{"class":80,"line":368},[78,530,531],{},"def validate_init_data(init_data: str, bot_token: str, max_age: int = 3600) -> dict:\n",[78,533,534],{"class":80,"line":374},[78,535,536],{},"    values = dict(parse_qsl(init_data, keep_blank_values=True))\n",[78,538,539],{"class":80,"line":380},[78,540,541],{},"    received_hash = values.pop(\"hash\", None)\n",[78,543,544],{"class":80,"line":385},[78,545,546],{},"    if not received_hash:\n",[78,548,549],{"class":80,"line":427},[78,550,551],{},"        raise TelegramAuthError(\"Missing hash\")\n",[78,553,554],{"class":80,"line":441},[78,555,144],{"emptyLinePlaceholder":143},[78,557,559],{"class":80,"line":558},18,[78,560,561],{},"    data_check_string = \"\\n\".join(\n",[78,563,565],{"class":80,"line":564},19,[78,566,567],{},"        f\"{key}={value}\" for key, value in sorted(values.items())\n",[78,569,571],{"class":80,"line":570},20,[78,572,573],{},"    )\n",[78,575,577],{"class":80,"line":576},21,[78,578,144],{"emptyLinePlaceholder":143},[78,580,582],{"class":80,"line":581},22,[78,583,584],{},"    secret_key = hmac.new(\n",[78,586,588],{"class":80,"line":587},23,[78,589,590],{},"        b\"WebAppData\",\n",[78,592,594],{"class":80,"line":593},24,[78,595,596],{},"        bot_token.encode(),\n",[78,598,600],{"class":80,"line":599},25,[78,601,602],{},"        hashlib.sha256,\n",[78,604,606],{"class":80,"line":605},26,[78,607,608],{},"    ).digest()\n",[78,610,612],{"class":80,"line":611},27,[78,613,614],{},"    calculated_hash = hmac.new(\n",[78,616,618],{"class":80,"line":617},28,[78,619,620],{},"        secret_key,\n",[78,622,624],{"class":80,"line":623},29,[78,625,626],{},"        data_check_string.encode(),\n",[78,628,630],{"class":80,"line":629},30,[78,631,602],{},[78,633,635],{"class":80,"line":634},31,[78,636,637],{},"    ).hexdigest()\n",[78,639,641],{"class":80,"line":640},32,[78,642,144],{"emptyLinePlaceholder":143},[78,644,646],{"class":80,"line":645},33,[78,647,648],{},"    if not hmac.compare_digest(calculated_hash, received_hash):\n",[78,650,652],{"class":80,"line":651},34,[78,653,654],{},"        raise TelegramAuthError(\"Invalid signature\")\n",[78,656,658],{"class":80,"line":657},35,[78,659,144],{"emptyLinePlaceholder":143},[78,661,663],{"class":80,"line":662},36,[78,664,665],{},"    auth_date = int(values.get(\"auth_date\", \"0\"))\n",[78,667,669],{"class":80,"line":668},37,[78,670,671],{},"    if auth_date \u003C= 0 or time.time() - auth_date > max_age:\n",[78,673,675],{"class":80,"line":674},38,[78,676,677],{},"        raise TelegramAuthError(\"initData expired\")\n",[78,679,681],{"class":80,"line":680},39,[78,682,144],{"emptyLinePlaceholder":143},[78,684,686],{"class":80,"line":685},40,[78,687,688],{},"    return json.loads(values[\"user\"])\n",[15,690,691,694,695,698],{},[19,692,693],{},"hmac.compare_digest"," нужен для безопасного сравнения. Проверка ",[19,696,697],{},"auth_date"," ограничивает повторное использование старой строки.",[43,700,702],{"id":701},"почему-возникает-invalid-signature","Почему возникает Invalid signature",[15,704,705],{},"Самая частая причина — изменить строку до проверки:",[51,707,709],{"className":471,"code":708,"language":473,"meta":57,"style":57},"# Так делать не нужно\ninit_data = unquote(init_data)\n",[19,710,711,716],{"__ignoreMap":57},[78,712,713],{"class":80,"line":81},[78,714,715],{},"# Так делать не нужно\n",[78,717,718],{"class":80,"line":140},[78,719,720],{},"init_data = unquote(init_data)\n",[15,722,723,726,727,730,731,734],{},[19,724,725],{},"parse_qsl"," уже корректно разбирает percent-encoding. Дополнительный ",[19,728,729],{},"unquote()"," может повторно декодировать символы внутри JSON и изменить ",[19,732,733],{},"data_check_string",". В итоге пользователь настоящий, но подпись больше не совпадает.",[15,736,737],{},"Другие причины:",[739,740,741,745,751,754,760,766],"ul",{},[742,743,744],"li",{},"проверка токеном другого бота;",[742,746,747,748,750],{},"исключение поля из ",[19,749,733],{},";",[742,752,753],{},"сортировка после изменения значений;",[742,755,756,757,759],{},"использование ",[19,758,21],{}," вместо исходной строки;",[742,761,762,763,750],{},"повторная кодировка пробелов и символа ",[19,764,765],{},"+",[742,767,768,769,34],{},"слишком маленький допустимый возраст ",[19,770,697],{},[43,772,774],{"id":773},"dependency-для-fastapi","Dependency для FastAPI",[15,776,777],{},"Проверку удобно оформить как dependency:",[51,779,781],{"className":471,"code":780,"language":473,"meta":57,"style":57},"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,782,783,788,792,796,801,806,811,816],{"__ignoreMap":57},[78,784,785],{"class":80,"line":81},[78,786,787],{},"from fastapi import Header, HTTPException\n",[78,789,790],{"class":80,"line":140},[78,791,144],{"emptyLinePlaceholder":143},[78,793,794],{"class":80,"line":147},[78,795,144],{"emptyLinePlaceholder":143},[78,797,798],{"class":80,"line":159},[78,799,800],{},"async def telegram_user(x_init_data: str = Header(alias=\"X-Init-Data\")) -> dict:\n",[78,802,803],{"class":80,"line":279},[78,804,805],{},"    try:\n",[78,807,808],{"class":80,"line":284},[78,809,810],{},"        return validate_init_data(x_init_data, settings.bot_token)\n",[78,812,813],{"class":80,"line":320},[78,814,815],{},"    except TelegramAuthError as exc:\n",[78,817,818],{"class":80,"line":329},[78,819,820],{},"        raise HTTPException(status_code=401, detail=str(exc)) from exc\n",[15,822,823],{},"Использование в endpoint:",[51,825,827],{"className":471,"code":826,"language":473,"meta":57,"style":57},"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,828,829,834,838,843,847,852,857],{"__ignoreMap":57},[78,830,831],{"class":80,"line":81},[78,832,833],{},"from fastapi import APIRouter, Depends\n",[78,835,836],{"class":80,"line":140},[78,837,144],{"emptyLinePlaceholder":143},[78,839,840],{"class":80,"line":147},[78,841,842],{},"router = APIRouter()\n",[78,844,845],{"class":80,"line":159},[78,846,144],{"emptyLinePlaceholder":143},[78,848,849],{"class":80,"line":279},[78,850,851],{},"@router.get(\"\u002Fprofile\")\n",[78,853,854],{"class":80,"line":284},[78,855,856],{},"async def profile(user: dict = Depends(telegram_user)):\n",[78,858,859],{"class":80,"line":320},[78,860,861],{},"    return await profiles.get_or_create(telegram_id=user[\"id\"])\n",[15,863,864],{},"Так один и тот же механизм защищает профиль, курсы, заказы и платежи.",[43,866,868],{"id":867},"что-проверить-перед-релизом","Что проверить перед релизом",[739,870,871,878,883,886,891,894,897],{},[742,872,873,874,877],{},"Mini App открывается через кнопку ",[19,875,876],{},"web_app",", а не обычную ссылку.",[742,879,880,882],{},[19,881,454],{}," отправляется без ручного декодирования.",[742,884,885],{},"Backend использует токен того же бота.",[742,887,888,889,34],{},"Проверяется ",[19,890,697],{},[742,892,893],{},"ID пользователя не принимается из JSON запроса.",[742,895,896],{},"API работает только через HTTPS.",[742,898,899],{},"Ошибка авторизации показывает понятный экран с повторным открытием Mini App.",[15,901,902],{},"Для локальной разработки сделайте отдельный явно включаемый preview-режим. Не ослабляйте production-проверку пустым заголовком или специальным Telegram ID.",[43,904,906],{"id":905},"следующий-шаг","Следующий шаг",[15,908,909,910,914,915,34],{},"Обзор возможностей Mini Apps есть в статье ",[28,911,913],{"href":912},"\u002Fru\u002Fblog\u002Ftelegram-mini-app-chto-eto\u002F","что такое Telegram Mini App",". Практическая разработка бота, Mini App, платежей и деплоя собрана в ",[28,916,918],{"href":917},"\u002Fru\u002Fcourses\u002Ftelegram-bots-python\u002F","полном курсе по Telegram-ботам",[15,920,921,922,34],{},"Нужен Mini App под запись, магазин или личный кабинет — ",[28,923,925],{"href":924},"\u002Fru\u002F#contact","опишите задачу",[927,928,929],"style",{},"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 .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 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":57,"searchDepth":140,"depth":140,"links":931},[932,933,934,935,936,937,938,939],{"id":45,"depth":140,"text":46},{"id":65,"depth":140,"text":66},{"id":175,"depth":140,"text":176},{"id":457,"depth":140,"text":458},{"id":701,"depth":140,"text":702},{"id":773,"depth":140,"text":774},{"id":867,"depth":140,"text":868},{"id":905,"depth":140,"text":906},"2026-08-24","Как связать Vue 3 Mini App с FastAPI, передать Telegram initData, проверить HMAC-подпись на сервере и не получить Invalid signature.","md",{},"\u002Fru\u002Fblog\u002Ftelegram-mini-app-vue-fastapi-initdata","13 мин",{"title":5,"description":941},"telegram-mini-app-vue-fastapi-initdata","ru\u002Fblog\u002Ftelegram-mini-app-vue-fastapi-initdata",[950,951,952,953,954],"Telegram Mini App","Vue.js","FastAPI","Python","Безопасность",null,"TYhieNmmQ32Yowgn-bF8E03enbDZEQ3svRqlNuHGFYg",1787594770904]