How to Accept Telegram Stars in a Bot or Mini App
Practice4 minInnovialabs editorial team
In short
The bot issues an invoice in XTR with no payment provider, approves the pre_checkout_query and delivers the goods only after successful_payment, storing telegram_payment_charge_id for idempotency and refunds via refundStarPayment.
The short answer
Telegram Stars are Telegram's in-app currency that bots and Mini Apps use to sell digital goods and services. The flow:
- The bot creates an invoice in the
XTRcurrency. No payment provider is involved, andprovider_tokenstays empty. - The user confirms, the bot receives a
pre_checkout_queryand answers yes or no. - Once the charge goes through, a message with
successful_paymentarrives. Only then do you deliver the goods. - If needed, you refund with
refundStarPayment.
When Stars are required
Under Telegram's rules, digital goods and services consumed inside Telegram are sold for Stars: content subscriptions, premium bot features, in-game items, access to materials. Physical goods and offline services are still paid through regular payment providers.
Invoice in a bot
The price is a single line item: for XTR, prices must contain exactly one element, and the amount is in whole Stars.
from aiogram import Bot, F, Router
from aiogram.types import LabeledPrice, Message, PreCheckoutQuery
router = Router()
@router.message(F.text == "/premium")
async def sell(message: Message):
order_id = await create_order(message.from_user.id, product="premium_30d")
await message.answer_invoice(
title="Premium for 30 days",
description="Extended bot features",
payload=f"order:{order_id}",
currency="XTR",
prices=[LabeledPrice(label="Premium", amount=100)],
)
@router.pre_checkout_query()
async def pre_checkout(query: PreCheckoutQuery):
ok = await order_is_open(query.invoice_payload)
await query.answer(ok=ok, error_message=None if ok else "This order is no longer available")
@router.message(F.successful_payment)
async def paid(message: Message):
sp = message.successful_payment
await deliver_once(
payload=sp.invoice_payload,
charge_id=sp.telegram_payment_charge_id,
user_id=message.from_user.id,
)
Payments in a Mini App
In a Mini App, the invoice opens without sending a chat message:
- Your server calls
createInvoiceLinkwith the same parameters (currency="XTR", one price,payloadwith the order ID) and returns the link to the frontend. - The frontend calls
Telegram.WebApp.openInvoice(url, callback). - The callback receives a status:
paid,cancelled,failedorpending.
The callback status is for the UI only: show a "Thank you" screen or bring the button back. Never deliver goods based on it. The source of truth is the successful_payment update your bot receives on the server. After payment, the Mini App simply asks the server for the current order status.
Delivering goods reliably
The most common problem is goods delivered twice or not at all. To prevent that:
- Create the order before the invoice and put its ID in
payload. Do not store price or contents inpayload; they are easy to mix up. - Store
telegram_payment_charge_idwith a unique index. A repeated update with the same ID is simply ignored. - Deliver inside a transaction: marking the order paid and granting access must succeed together or not at all.
- Check everything in
pre_checkout_querythat could block delivery: the order is not already paid, the item is available, the user is not banned. Answer fast, or the payment is cancelled. - Make delivery retryable: if sending the goods fails, the job should rerun without creating a new payment.
Refunds
refundStarPayment takes user_id and telegram_payment_charge_id and returns the Stars to the user. Practical rules:
- Before refunding, revoke the access you granted and mark the order as refunded.
- A payment cannot be refunded twice, so check the order status on your side.
- Describe your refund policy in the bot and provide a clear command for payment questions. Telegram expects bots that take payments to respond to users about them (usually via a
/paysupportcommand).
For reconciliation, use getStarTransactions: it returns the bot's incoming and outgoing transactions, which makes it easy to spot mismatches with your database.
Common mistakes
- Several items in
pricesforXTR: Telegram returns an error. - Delivering goods in the
pre_checkout_queryhandler or from the Mini App callback. - No uniqueness constraint on
telegram_payment_charge_id. - Testing only in production. Use the Telegram test server to rehearse the flows.
Details are in the Telegram Stars payments documentation.