[{"data":1,"prerenderedAt":622},["ShallowReactive",2],{"blog-en-telegram-stars-aiogram-3-payments":3},{"id":4,"title":5,"body":6,"date":605,"description":606,"extension":607,"meta":608,"navigation":197,"path":609,"readTime":610,"seo":611,"slug":612,"stem":613,"tags":614,"updated":620,"__hash__":621},"blogEn\u002Fen\u002Fblog\u002Ftelegram-stars-aiogram-3-payments.md","Telegram Stars with Aiogram 3: Payments, Validation, and Refunds",{"type":7,"value":8,"toc":596},"minimark",[9,14,31,34,39,42,78,81,85,88,165,172,176,244,247,251,370,377,381,485,495,499,506,531,538,545,549,572,585,592],[10,11,13],"h1",{"id":12},"telegram-stars-with-aiogram-3","Telegram Stars with Aiogram 3",[15,16,17,18,22,23,30],"p",{},"Telegram Stars are the required payment method for digital goods and services sold inside Telegram, including course access, subscriptions, files, and paid Mini App features. These payments use the ",[19,20,21],"code",{},"XTR"," currency. Telegram documents the requirement and the complete payment sequence in its ",[24,25,29],"a",{"href":26,"rel":27},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fpayments-stars",[28],"nofollow","official Stars guide",".",[15,32,33],{},"This guide focuses on the part that matters in production: creating a server-side order, validating every payment field, preventing duplicate delivery, and keeping refunds possible.",[35,36,38],"h2",{"id":37},"the-payment-flow","The payment flow",[15,40,41],{},"A reliable flow has six steps:",[43,44,45,53,56,62,68,75],"ol",{},[46,47,48,49,52],"li",{},"The backend creates a ",[19,50,51],{},"pending"," order.",[46,54,55],{},"The backend reads the price from its own product catalog.",[46,57,58,59,61],{},"The bot creates an ",[19,60,21],{}," invoice.",[46,63,64,65,30],{},"The backend validates the incoming ",[19,66,67],{},"pre_checkout_query",[46,69,70,71,74],{},"It records ",[19,72,73],{},"successful_payment"," and the Telegram charge ID.",[46,76,77],{},"It grants access idempotently.",[15,79,80],{},"The frontend should only choose a product. It must never submit the authoritative price, buyer ID, or payment status.",[35,82,84],{"id":83},"store-an-order-first","Store an order first",[15,86,87],{},"Even a small project needs a payment table:",[89,90,95],"pre",{"className":91,"code":92,"language":93,"meta":94,"style":94},"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,96,97,105,111,117,123,129,135,141,147,153,159],{"__ignoreMap":94},[98,99,102],"span",{"class":100,"line":101},"line",1,[98,103,104],{},"CREATE TABLE payments (\n",[98,106,108],{"class":100,"line":107},2,[98,109,110],{},"    id TEXT PRIMARY KEY,\n",[98,112,114],{"class":100,"line":113},3,[98,115,116],{},"    user_id INTEGER NOT NULL,\n",[98,118,120],{"class":100,"line":119},4,[98,121,122],{},"    product_slug TEXT NOT NULL,\n",[98,124,126],{"class":100,"line":125},5,[98,127,128],{},"    amount INTEGER NOT NULL,\n",[98,130,132],{"class":100,"line":131},6,[98,133,134],{},"    currency TEXT NOT NULL,\n",[98,136,138],{"class":100,"line":137},7,[98,139,140],{},"    status TEXT NOT NULL DEFAULT 'pending',\n",[98,142,144],{"class":100,"line":143},8,[98,145,146],{},"    telegram_charge_id TEXT UNIQUE,\n",[98,148,150],{"class":100,"line":149},9,[98,151,152],{},"    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n",[98,154,156],{"class":100,"line":155},10,[98,157,158],{},"    paid_at TEXT\n",[98,160,162],{"class":100,"line":161},11,[98,163,164],{},");\n",[15,166,167,168,171],{},"Stars are stored as integers. A price of 500 Stars is saved as ",[19,169,170],{},"500",", without floating-point conversion.",[35,173,175],{"id":174},"create-an-invoice-link","Create an invoice link",[89,177,181],{"className":178,"code":179,"language":180,"meta":94,"style":94},"language-python shiki shiki-themes github-light github-dark","from aiogram import Bot\nfrom aiogram.types import LabeledPrice\n\n\nasync def create_course_invoice(bot: Bot, order_id: str, stars: int) -> str:\n    return await bot.create_invoice_link(\n        title=\"Telegram Bots with Python\",\n        description=\"Lifetime access to 12 lessons and four projects\",\n        payload=f\"course:{order_id}\",\n        currency=\"XTR\",\n        prices=[LabeledPrice(label=\"Course access\", amount=stars)],\n    )\n","python",[19,182,183,188,193,199,203,208,213,218,223,228,233,238],{"__ignoreMap":94},[98,184,185],{"class":100,"line":101},[98,186,187],{},"from aiogram import Bot\n",[98,189,190],{"class":100,"line":107},[98,191,192],{},"from aiogram.types import LabeledPrice\n",[98,194,195],{"class":100,"line":113},[98,196,198],{"emptyLinePlaceholder":197},true,"\n",[98,200,201],{"class":100,"line":119},[98,202,198],{"emptyLinePlaceholder":197},[98,204,205],{"class":100,"line":125},[98,206,207],{},"async def create_course_invoice(bot: Bot, order_id: str, stars: int) -> str:\n",[98,209,210],{"class":100,"line":131},[98,211,212],{},"    return await bot.create_invoice_link(\n",[98,214,215],{"class":100,"line":137},[98,216,217],{},"        title=\"Telegram Bots with Python\",\n",[98,219,220],{"class":100,"line":143},[98,221,222],{},"        description=\"Lifetime access to 12 lessons and four projects\",\n",[98,224,225],{"class":100,"line":149},[98,226,227],{},"        payload=f\"course:{order_id}\",\n",[98,229,230],{"class":100,"line":155},[98,231,232],{},"        currency=\"XTR\",\n",[98,234,235],{"class":100,"line":161},[98,236,237],{},"        prices=[LabeledPrice(label=\"Course access\", amount=stars)],\n",[98,239,241],{"class":100,"line":240},12,[98,242,243],{},"    )\n",[15,245,246],{},"Stars do not need an external provider token. Keep the payload opaque: use an internal order ID rather than a client-controlled JSON object.",[35,248,250],{"id":249},"validate-pre_checkout_query","Validate pre_checkout_query",[89,252,254],{"className":178,"code":253,"language":180,"meta":94,"style":94},"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 \"This order has expired. Create a new invoice.\",\n    )\n",[19,255,256,261,266,270,275,279,284,289,294,299,303,308,313,319,325,331,337,342,347,353,359,365],{"__ignoreMap":94},[98,257,258],{"class":100,"line":101},[98,259,260],{},"from aiogram import Router\n",[98,262,263],{"class":100,"line":107},[98,264,265],{},"from aiogram.types import PreCheckoutQuery\n",[98,267,268],{"class":100,"line":113},[98,269,198],{"emptyLinePlaceholder":197},[98,271,272],{"class":100,"line":119},[98,273,274],{},"router = Router()\n",[98,276,277],{"class":100,"line":125},[98,278,198],{"emptyLinePlaceholder":197},[98,280,281],{"class":100,"line":131},[98,282,283],{},"@router.pre_checkout_query()\n",[98,285,286],{"class":100,"line":137},[98,287,288],{},"async def process_pre_checkout(query: PreCheckoutQuery):\n",[98,290,291],{"class":100,"line":143},[98,292,293],{},"    order_id = query.invoice_payload.removeprefix(\"course:\")\n",[98,295,296],{"class":100,"line":149},[98,297,298],{},"    order = await payments.get(order_id)\n",[98,300,301],{"class":100,"line":155},[98,302,198],{"emptyLinePlaceholder":197},[98,304,305],{"class":100,"line":161},[98,306,307],{},"    valid = bool(\n",[98,309,310],{"class":100,"line":240},[98,311,312],{},"        order\n",[98,314,316],{"class":100,"line":315},13,[98,317,318],{},"        and order.status == \"pending\"\n",[98,320,322],{"class":100,"line":321},14,[98,323,324],{},"        and order.user_id == query.from_user.id\n",[98,326,328],{"class":100,"line":327},15,[98,329,330],{},"        and order.currency == \"XTR\"\n",[98,332,334],{"class":100,"line":333},16,[98,335,336],{},"        and order.amount == query.total_amount\n",[98,338,340],{"class":100,"line":339},17,[98,341,243],{},[98,343,345],{"class":100,"line":344},18,[98,346,198],{"emptyLinePlaceholder":197},[98,348,350],{"class":100,"line":349},19,[98,351,352],{},"    await query.answer(\n",[98,354,356],{"class":100,"line":355},20,[98,357,358],{},"        ok=valid,\n",[98,360,362],{"class":100,"line":361},21,[98,363,364],{},"        error_message=None if valid else \"This order has expired. Create a new invoice.\",\n",[98,366,368],{"class":100,"line":367},22,[98,369,243],{},[15,371,372,373,376],{},"Do not answer every query with ",[19,374,375],{},"ok=True",". Check the user, amount, currency, status, and the order referenced by the payload.",[35,378,380],{"id":379},"process-successful_payment-once","Process successful_payment once",[89,382,384],{"className":178,"code":383,"language":180,"meta":94,"style":94},"from aiogram import F\nfrom aiogram.types import Message\n\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(\"Payment confirmed. Your access is ready.\")\n",[19,385,386,391,396,400,404,409,414,419,424,428,433,438,443,448,453,458,462,466,471,476,480],{"__ignoreMap":94},[98,387,388],{"class":100,"line":101},[98,389,390],{},"from aiogram import F\n",[98,392,393],{"class":100,"line":107},[98,394,395],{},"from aiogram.types import Message\n",[98,397,398],{"class":100,"line":113},[98,399,198],{"emptyLinePlaceholder":197},[98,401,402],{"class":100,"line":119},[98,403,198],{"emptyLinePlaceholder":197},[98,405,406],{"class":100,"line":125},[98,407,408],{},"@router.message(F.successful_payment)\n",[98,410,411],{"class":100,"line":131},[98,412,413],{},"async def process_successful_payment(message: Message):\n",[98,415,416],{"class":100,"line":137},[98,417,418],{},"    payment = message.successful_payment\n",[98,420,421],{"class":100,"line":143},[98,422,423],{},"    order_id = payment.invoice_payload.removeprefix(\"course:\")\n",[98,425,426],{"class":100,"line":149},[98,427,198],{"emptyLinePlaceholder":197},[98,429,430],{"class":100,"line":155},[98,431,432],{},"    order = await payments.mark_paid_once(\n",[98,434,435],{"class":100,"line":161},[98,436,437],{},"        order_id=order_id,\n",[98,439,440],{"class":100,"line":240},[98,441,442],{},"        user_id=message.from_user.id,\n",[98,444,445],{"class":100,"line":315},[98,446,447],{},"        amount=payment.total_amount,\n",[98,449,450],{"class":100,"line":321},[98,451,452],{},"        currency=payment.currency,\n",[98,454,455],{"class":100,"line":327},[98,456,457],{},"        charge_id=payment.telegram_payment_charge_id,\n",[98,459,460],{"class":100,"line":333},[98,461,243],{},[98,463,464],{"class":100,"line":339},[98,465,198],{"emptyLinePlaceholder":197},[98,467,468],{"class":100,"line":344},[98,469,470],{},"    if order.just_paid:\n",[98,472,473],{"class":100,"line":349},[98,474,475],{},"        await courses.grant_access(order.user_id, order.product_slug)\n",[98,477,478],{"class":100,"line":355},[98,479,198],{"emptyLinePlaceholder":197},[98,481,482],{"class":100,"line":361},[98,483,484],{},"    await message.answer(\"Payment confirmed. Your access is ready.\")\n",[15,486,487,490,491,494],{},[19,488,489],{},"mark_paid_once"," should run in a database transaction. A unique constraint on ",[19,492,493],{},"telegram_charge_id"," prevents duplicate delivery if Telegram retries an update or the bot restarts during processing.",[35,496,498],{"id":497},"refund-a-stars-payment","Refund a Stars payment",[15,500,501,502,505],{},"Keep ",[19,503,504],{},"telegram_payment_charge_id","; the Bot API needs it for a refund.",[89,507,509],{"className":178,"code":508,"language":180,"meta":94,"style":94},"await bot.refund_star_payment(\n    user_id=order.user_id,\n    telegram_payment_charge_id=order.telegram_charge_id,\n)\n",[19,510,511,516,521,526],{"__ignoreMap":94},[98,512,513],{"class":100,"line":101},[98,514,515],{},"await bot.refund_star_payment(\n",[98,517,518],{"class":100,"line":107},[98,519,520],{},"    user_id=order.user_id,\n",[98,522,523],{"class":100,"line":113},[98,524,525],{},"    telegram_payment_charge_id=order.telegram_charge_id,\n",[98,527,528],{"class":100,"line":119},[98,529,530],{},")\n",[15,532,533,534,537],{},"After Telegram confirms the refund, mark the order as ",[19,535,536],{},"refunded"," and recompute access. Do not revoke access if the user still has another active purchase or a free entitlement.",[15,539,540,541,544],{},"Bots selling digital goods also need a working ",[19,542,543],{},"\u002Fpaysupport"," command and timely payment support.",[35,546,548],{"id":547},"common-mistakes","Common mistakes",[550,551,552,555,558,563,566,569],"ul",{},[46,553,554],{},"Trusting a price sent by a Mini App.",[46,556,557],{},"Approving pre-checkout without validating the order.",[46,559,560,561,30],{},"Delivering before ",[19,562,73],{},[46,564,565],{},"Losing the Telegram charge ID.",[46,567,568],{},"Processing the same payment more than once.",[46,570,571],{},"Putting access rules directly inside Telegram handlers.",[15,573,574,575,579,580,584],{},"For a first project, start with the ",[24,576,578],{"href":577},"\u002Fen\u002Fcourses\u002Faiogram-start\u002F","free Aiogram 3 workshop",". The ",[24,581,583],{"href":582},"\u002Fen\u002Fcourses\u002Ftelegram-bots-python\u002F","complete Telegram bot course"," covers PostgreSQL, Stars, Mini Apps, testing, and Docker deployment.",[15,586,587,588,30],{},"For an existing commercial bot, ",[24,589,591],{"href":590},"\u002Fen\u002F#contact","describe the integration you need",[593,594,595],"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":94,"searchDepth":107,"depth":107,"links":597},[598,599,600,601,602,603,604],{"id":37,"depth":107,"text":38},{"id":83,"depth":107,"text":84},{"id":174,"depth":107,"text":175},{"id":249,"depth":107,"text":250},{"id":379,"depth":107,"text":380},{"id":497,"depth":107,"text":498},{"id":547,"depth":107,"text":548},"2026-08-24","A production-minded guide to Telegram Stars in Python and Aiogram 3: invoices, pre-checkout validation, successful payments, idempotency, and refunds.","md",{},"\u002Fen\u002Fblog\u002Ftelegram-stars-aiogram-3-payments","10 min",{"title":5,"description":606},"telegram-stars-aiogram-3-payments","en\u002Fblog\u002Ftelegram-stars-aiogram-3-payments",[615,616,617,618,619],"Telegram Stars","Aiogram","Python","Payments","Telegram",null,"ixsQUXgqjK5KPiHQipZ3fsZ79MpXAgNnV1xR_Dadk7o",1787594770991]