[{"data":1,"prerenderedAt":643},["ShallowReactive",2],{"blog-ru-telegram-stars-aiogram-3-payments":3},{"id":4,"title":5,"body":6,"date":626,"description":627,"extension":628,"meta":629,"navigation":205,"path":630,"readTime":631,"seo":632,"slug":633,"stem":634,"tags":635,"updated":641,"__hash__":642},"blogRu\u002Fru\u002Fblog\u002Ftelegram-stars-aiogram-3-payments.md","Telegram Stars в Aiogram 3: оплата, проверка и возвраты",{"type":7,"value":8,"toc":616},"minimark",[9,14,31,34,39,42,77,80,84,87,164,177,181,184,247,254,258,265,385,388,392,395,495,505,509,516,541,548,555,559,586,589,593,605,612],[10,11,13],"h1",{"id":12},"telegram-stars-в-aiogram-3-оплата-без-уязвимостей","Telegram Stars в Aiogram 3: оплата без уязвимостей",[15,16,17,18,22,23,30],"p",{},"Telegram Stars нужны для продажи цифровых товаров и услуг внутри Telegram: доступа к курсу, подписки, файлов, функций Mini App. Для таких покупок бот использует валюту ",[19,20,21],"code",{},"XTR",", а сторонние платёжные провайдеры внутри Telegram применять нельзя. Это прямо указано в ",[24,25,29],"a",{"href":26,"rel":27},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fpayments-stars",[28],"nofollow","официальной документации Telegram",".",[15,32,33],{},"Ниже не просто кнопка «Оплатить», а полный безопасный поток: локальный заказ, проверка суммы, защита от повторной выдачи и возврат.",[35,36,38],"h2",{"id":37},"как-устроен-платёжный-поток","Как устроен платёжный поток",[15,40,41],{},"Надёжная схема выглядит так:",[43,44,45,52,55,60,67,74],"ol",{},[46,47,48,49,30],"li",{},"Backend создаёт заказ со статусом ",[19,50,51],{},"pending",[46,53,54],{},"Цена берётся из серверного каталога, а не из запроса клиента.",[46,56,57,58,30],{},"Бот создаёт invoice в валюте ",[19,59,21],{},[46,61,62,63,66],{},"На ",[19,64,65],{},"pre_checkout_query"," backend повторно проверяет заказ.",[46,68,69,70,73],{},"После ",[19,71,72],{},"successful_payment"," сохраняется идентификатор платежа.",[46,75,76],{},"Доступ выдаётся идемпотентно: повторный update ничего не выдаёт второй раз.",[15,78,79],{},"Клиенту нельзя разрешать передавать цену или Telegram ID покупателя. Он выбирает только товар, всё остальное определяет сервер.",[35,81,83],{"id":82},"модель-заказа","Модель заказа",[15,85,86],{},"Для небольшого проекта достаточно такой структуры:",[88,89,94],"pre",{"className":90,"code":91,"language":92,"meta":93,"style":93},"language-sql shiki shiki-themes github-light github-dark","CREATE TABLE payments (\n    id TEXT PRIMARY KEY,\n    user_id INTEGER NOT NULL,\n    product_slug TEXT NOT NULL,\n    amount INTEGER NOT NULL,\n    currency TEXT NOT NULL,\n    status TEXT NOT NULL DEFAULT 'pending',\n    telegram_charge_id TEXT UNIQUE,\n    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    paid_at TEXT\n);\n","sql","",[19,95,96,104,110,116,122,128,134,140,146,152,158],{"__ignoreMap":93},[97,98,101],"span",{"class":99,"line":100},"line",1,[97,102,103],{},"CREATE TABLE payments (\n",[97,105,107],{"class":99,"line":106},2,[97,108,109],{},"    id TEXT PRIMARY KEY,\n",[97,111,113],{"class":99,"line":112},3,[97,114,115],{},"    user_id INTEGER NOT NULL,\n",[97,117,119],{"class":99,"line":118},4,[97,120,121],{},"    product_slug TEXT NOT NULL,\n",[97,123,125],{"class":99,"line":124},5,[97,126,127],{},"    amount INTEGER NOT NULL,\n",[97,129,131],{"class":99,"line":130},6,[97,132,133],{},"    currency TEXT NOT NULL,\n",[97,135,137],{"class":99,"line":136},7,[97,138,139],{},"    status TEXT NOT NULL DEFAULT 'pending',\n",[97,141,143],{"class":99,"line":142},8,[97,144,145],{},"    telegram_charge_id TEXT UNIQUE,\n",[97,147,149],{"class":99,"line":148},9,[97,150,151],{},"    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n",[97,153,155],{"class":99,"line":154},10,[97,156,157],{},"    paid_at TEXT\n",[97,159,161],{"class":99,"line":160},11,[97,162,163],{},");\n",[15,165,166,169,170,173,174,30],{},[19,167,168],{},"amount"," для Stars хранится целым числом. Если товар стоит 500 Stars, в заказе должно быть ",[19,171,172],{},"500",", без копеек и пересчёта через ",[19,175,176],{},"float",[35,178,180],{"id":179},"создание-invoice-link","Создание invoice link",[15,182,183],{},"Пример для Aiogram 3:",[88,185,189],{"className":186,"code":187,"language":188,"meta":93,"style":93},"language-python shiki shiki-themes github-light github-dark","from aiogram import Bot\nfrom aiogram.types import LabeledPrice\n\nasync def create_course_invoice(bot: Bot, order_id: str, stars: int) -> str:\n    return await bot.create_invoice_link(\n        title=\"Telegram-боты на Python\",\n        description=\"Бессрочный доступ к 12 урокам и четырём проектам\",\n        payload=f\"course:{order_id}\",\n        currency=\"XTR\",\n        prices=[LabeledPrice(label=\"Доступ к курсу\", amount=stars)],\n    )\n","python",[19,190,191,196,201,207,212,217,222,227,232,237,242],{"__ignoreMap":93},[97,192,193],{"class":99,"line":100},[97,194,195],{},"from aiogram import Bot\n",[97,197,198],{"class":99,"line":106},[97,199,200],{},"from aiogram.types import LabeledPrice\n",[97,202,203],{"class":99,"line":112},[97,204,206],{"emptyLinePlaceholder":205},true,"\n",[97,208,209],{"class":99,"line":118},[97,210,211],{},"async def create_course_invoice(bot: Bot, order_id: str, stars: int) -> str:\n",[97,213,214],{"class":99,"line":124},[97,215,216],{},"    return await bot.create_invoice_link(\n",[97,218,219],{"class":99,"line":130},[97,220,221],{},"        title=\"Telegram-боты на Python\",\n",[97,223,224],{"class":99,"line":136},[97,225,226],{},"        description=\"Бессрочный доступ к 12 урокам и четырём проектам\",\n",[97,228,229],{"class":99,"line":142},[97,230,231],{},"        payload=f\"course:{order_id}\",\n",[97,233,234],{"class":99,"line":148},[97,235,236],{},"        currency=\"XTR\",\n",[97,238,239],{"class":99,"line":154},[97,240,241],{},"        prices=[LabeledPrice(label=\"Доступ к курсу\", amount=stars)],\n",[97,243,244],{"class":99,"line":160},[97,245,246],{},"    )\n",[15,248,249,250,253],{},"Для Stars не нужен токен внешнего провайдера. В ",[19,251,252],{},"payload"," передаётся непрозрачный ID локального заказа, а не JSON с ценой и пользователем.",[35,255,257],{"id":256},"проверка-pre_checkout_query","Проверка pre_checkout_query",[15,259,260,261,264],{},"Telegram ждёт ответ на pre-checkout ограниченное время, поэтому обработчик должен быть коротким. Но отвечать ",[19,262,263],{},"ok=True"," без проверки нельзя.",[88,266,268],{"className":186,"code":267,"language":188,"meta":93,"style":93},"from aiogram import Router\nfrom aiogram.types import PreCheckoutQuery\n\nrouter = Router()\n\n@router.pre_checkout_query()\nasync def process_pre_checkout(query: PreCheckoutQuery):\n    order_id = query.invoice_payload.removeprefix(\"course:\")\n    order = await payments.get(order_id)\n\n    valid = bool(\n        order\n        and order.status == \"pending\"\n        and order.user_id == query.from_user.id\n        and order.currency == \"XTR\"\n        and order.amount == query.total_amount\n    )\n\n    await query.answer(\n        ok=valid,\n        error_message=None if valid else \"Заказ устарел. Создайте новый счёт.\",\n    )\n",[19,269,270,275,280,284,289,293,298,303,308,313,317,322,328,334,340,346,352,357,362,368,374,380],{"__ignoreMap":93},[97,271,272],{"class":99,"line":100},[97,273,274],{},"from aiogram import Router\n",[97,276,277],{"class":99,"line":106},[97,278,279],{},"from aiogram.types import PreCheckoutQuery\n",[97,281,282],{"class":99,"line":112},[97,283,206],{"emptyLinePlaceholder":205},[97,285,286],{"class":99,"line":118},[97,287,288],{},"router = Router()\n",[97,290,291],{"class":99,"line":124},[97,292,206],{"emptyLinePlaceholder":205},[97,294,295],{"class":99,"line":130},[97,296,297],{},"@router.pre_checkout_query()\n",[97,299,300],{"class":99,"line":136},[97,301,302],{},"async def process_pre_checkout(query: PreCheckoutQuery):\n",[97,304,305],{"class":99,"line":142},[97,306,307],{},"    order_id = query.invoice_payload.removeprefix(\"course:\")\n",[97,309,310],{"class":99,"line":148},[97,311,312],{},"    order = await payments.get(order_id)\n",[97,314,315],{"class":99,"line":154},[97,316,206],{"emptyLinePlaceholder":205},[97,318,319],{"class":99,"line":160},[97,320,321],{},"    valid = bool(\n",[97,323,325],{"class":99,"line":324},12,[97,326,327],{},"        order\n",[97,329,331],{"class":99,"line":330},13,[97,332,333],{},"        and order.status == \"pending\"\n",[97,335,337],{"class":99,"line":336},14,[97,338,339],{},"        and order.user_id == query.from_user.id\n",[97,341,343],{"class":99,"line":342},15,[97,344,345],{},"        and order.currency == \"XTR\"\n",[97,347,349],{"class":99,"line":348},16,[97,350,351],{},"        and order.amount == query.total_amount\n",[97,353,355],{"class":99,"line":354},17,[97,356,246],{},[97,358,360],{"class":99,"line":359},18,[97,361,206],{"emptyLinePlaceholder":205},[97,363,365],{"class":99,"line":364},19,[97,366,367],{},"    await query.answer(\n",[97,369,371],{"class":99,"line":370},20,[97,372,373],{},"        ok=valid,\n",[97,375,377],{"class":99,"line":376},21,[97,378,379],{},"        error_message=None if valid else \"Заказ устарел. Создайте новый счёт.\",\n",[97,381,383],{"class":99,"line":382},22,[97,384,246],{},[15,386,387],{},"Проверяются пользователь, валюта, сумма, статус и связь payload с заказом. Так подмена запроса на frontend не изменит стоимость товара.",[35,389,391],{"id":390},"обработка-successful_payment","Обработка successful_payment",[15,393,394],{},"Успешный платёж приходит как service message. Сначала фиксируем его в базе, затем выдаём доступ.",[88,396,398],{"className":186,"code":397,"language":188,"meta":93,"style":93},"from aiogram import F\nfrom aiogram.types import Message\n\n@router.message(F.successful_payment)\nasync def process_successful_payment(message: Message):\n    payment = message.successful_payment\n    order_id = payment.invoice_payload.removeprefix(\"course:\")\n\n    order = await payments.mark_paid_once(\n        order_id=order_id,\n        user_id=message.from_user.id,\n        amount=payment.total_amount,\n        currency=payment.currency,\n        charge_id=payment.telegram_payment_charge_id,\n    )\n\n    if order.just_paid:\n        await courses.grant_access(order.user_id, order.product_slug)\n\n    await message.answer(\"Оплата подтверждена. Доступ открыт.\")\n",[19,399,400,405,410,414,419,424,429,434,438,443,448,453,458,463,468,472,476,481,486,490],{"__ignoreMap":93},[97,401,402],{"class":99,"line":100},[97,403,404],{},"from aiogram import F\n",[97,406,407],{"class":99,"line":106},[97,408,409],{},"from aiogram.types import Message\n",[97,411,412],{"class":99,"line":112},[97,413,206],{"emptyLinePlaceholder":205},[97,415,416],{"class":99,"line":118},[97,417,418],{},"@router.message(F.successful_payment)\n",[97,420,421],{"class":99,"line":124},[97,422,423],{},"async def process_successful_payment(message: Message):\n",[97,425,426],{"class":99,"line":130},[97,427,428],{},"    payment = message.successful_payment\n",[97,430,431],{"class":99,"line":136},[97,432,433],{},"    order_id = payment.invoice_payload.removeprefix(\"course:\")\n",[97,435,436],{"class":99,"line":142},[97,437,206],{"emptyLinePlaceholder":205},[97,439,440],{"class":99,"line":148},[97,441,442],{},"    order = await payments.mark_paid_once(\n",[97,444,445],{"class":99,"line":154},[97,446,447],{},"        order_id=order_id,\n",[97,449,450],{"class":99,"line":160},[97,451,452],{},"        user_id=message.from_user.id,\n",[97,454,455],{"class":99,"line":324},[97,456,457],{},"        amount=payment.total_amount,\n",[97,459,460],{"class":99,"line":330},[97,461,462],{},"        currency=payment.currency,\n",[97,464,465],{"class":99,"line":336},[97,466,467],{},"        charge_id=payment.telegram_payment_charge_id,\n",[97,469,470],{"class":99,"line":342},[97,471,246],{},[97,473,474],{"class":99,"line":348},[97,475,206],{"emptyLinePlaceholder":205},[97,477,478],{"class":99,"line":354},[97,479,480],{},"    if order.just_paid:\n",[97,482,483],{"class":99,"line":359},[97,484,485],{},"        await courses.grant_access(order.user_id, order.product_slug)\n",[97,487,488],{"class":99,"line":364},[97,489,206],{"emptyLinePlaceholder":205},[97,491,492],{"class":99,"line":370},[97,493,494],{},"    await message.answer(\"Оплата подтверждена. Доступ открыт.\")\n",[15,496,497,500,501,504],{},[19,498,499],{},"mark_paid_once"," должен выполняться в транзакции. Уникальный индекс на ",[19,502,503],{},"telegram_charge_id"," защищает от повторной обработки одного платежа после рестарта или повторной доставки update.",[35,506,508],{"id":507},"возврат-stars","Возврат Stars",[15,510,511,512,515],{},"Сохраняйте ",[19,513,514],{},"telegram_payment_charge_id",": он нужен для возврата через Bot API.",[88,517,519],{"className":186,"code":518,"language":188,"meta":93,"style":93},"await bot.refund_star_payment(\n    user_id=order.user_id,\n    telegram_payment_charge_id=order.telegram_charge_id,\n)\n",[19,520,521,526,531,536],{"__ignoreMap":93},[97,522,523],{"class":99,"line":100},[97,524,525],{},"await bot.refund_star_payment(\n",[97,527,528],{"class":99,"line":106},[97,529,530],{},"    user_id=order.user_id,\n",[97,532,533],{"class":99,"line":112},[97,534,535],{},"    telegram_payment_charge_id=order.telegram_charge_id,\n",[97,537,538],{"class":99,"line":118},[97,539,540],{},")\n",[15,542,543,544,547],{},"После успешного ответа пометьте заказ как ",[19,545,546],{},"refunded"," и пересчитайте доступ. Если пользователь купил один продукт дважды, возврат только одной покупки не должен автоматически закрывать вторую.",[15,549,550,551,554],{},"Telegram также требует от бота, продающего цифровые товары, обрабатывать команду ",[19,552,553],{},"\u002Fpaysupport"," и помогать с платёжными вопросами.",[35,556,558],{"id":557},"что-чаще-всего-ломают","Что чаще всего ломают",[560,561,562,565,570,575,580,583],"ul",{},[46,563,564],{},"Доверяют сумме, присланной Mini App.",[46,566,567,568,30],{},"Безусловно подтверждают любой ",[19,569,65],{},[46,571,572,573,30],{},"Выдают товар до ",[19,574,72],{},[46,576,577,578,30],{},"Не сохраняют ",[19,579,514],{},[46,581,582],{},"Повторно выдают товар при повторном update.",[46,584,585],{},"Смешивают бизнес-логику доступа с Telegram handler.",[15,587,588],{},"Handler должен только проверить входные данные и вызвать платёжный сервис. Каталог, заказы и выдача доступа остаются на backend.",[35,590,592],{"id":591},"куда-двигаться-дальше","Куда двигаться дальше",[15,594,595,596,600,601,30],{},"Для первого бота начните с ",[24,597,599],{"href":598},"\u002Fru\u002Fcourses\u002Faiogram-start\u002F","бесплатного практикума по Aiogram 3",". Полный поток с PostgreSQL, платежами, Mini App, тестами и Docker разобран в ",[24,602,604],{"href":603},"\u002Fru\u002Fcourses\u002Ftelegram-bots-python\u002F","курсе по коммерческим Telegram-ботам",[15,606,607,608,30],{},"Если нужна интеграция платежей в готовый проект, можно ",[24,609,611],{"href":610},"\u002Fru\u002F#contact","обсудить разработку",[613,614,615],"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);}",{"title":93,"searchDepth":106,"depth":106,"links":617},[618,619,620,621,622,623,624,625],{"id":37,"depth":106,"text":38},{"id":82,"depth":106,"text":83},{"id":179,"depth":106,"text":180},{"id":256,"depth":106,"text":257},{"id":390,"depth":106,"text":391},{"id":507,"depth":106,"text":508},{"id":557,"depth":106,"text":558},{"id":591,"depth":106,"text":592},"2026-08-24","Практическая интеграция Telegram Stars в бота на Python и Aiogram 3: invoice, pre_checkout_query, successful_payment, идемпотентность и возврат платежа.","md",{},"\u002Fru\u002Fblog\u002Ftelegram-stars-aiogram-3-payments","11 мин",{"title":5,"description":627},"telegram-stars-aiogram-3-payments","ru\u002Fblog\u002Ftelegram-stars-aiogram-3-payments",[636,637,638,639,640],"Telegram Stars","Aiogram","Python","Платежи","Telegram",null,"Yx2rcGZtOuO_fTHyiBIitdOLj7QzAvwFu2B3OY1I2G4",1787594770920]