all articles
August 24, 2026·10 min

Telegram Stars with Aiogram 3: Payments, Validation, and Refunds

A production-minded guide to Telegram Stars in Python and Aiogram 3: invoices, pre-checkout validation, successful payments, idempotency, and refunds.

Telegram StarsAiogramPythonPaymentsTelegram

Telegram Stars with Aiogram 3

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 XTR currency. Telegram documents the requirement and the complete payment sequence in its official Stars guide.

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.

The payment flow

A reliable flow has six steps:

  1. The backend creates a pending order.
  2. The backend reads the price from its own product catalog.
  3. The bot creates an XTR invoice.
  4. The backend validates the incoming pre_checkout_query.
  5. It records successful_payment and the Telegram charge ID.
  6. It grants access idempotently.

The frontend should only choose a product. It must never submit the authoritative price, buyer ID, or payment status.

Store an order first

Even a small project needs a payment table:

CREATE TABLE payments (
    id TEXT PRIMARY KEY,
    user_id INTEGER NOT NULL,
    product_slug TEXT NOT NULL,
    amount INTEGER NOT NULL,
    currency TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',
    telegram_charge_id TEXT UNIQUE,
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    paid_at TEXT
);

Stars are stored as integers. A price of 500 Stars is saved as 500, without floating-point conversion.

from aiogram import Bot
from aiogram.types import LabeledPrice


async def create_course_invoice(bot: Bot, order_id: str, stars: int) -> str:
    return await bot.create_invoice_link(
        title="Telegram Bots with Python",
        description="Lifetime access to 12 lessons and four projects",
        payload=f"course:{order_id}",
        currency="XTR",
        prices=[LabeledPrice(label="Course access", amount=stars)],
    )

Stars do not need an external provider token. Keep the payload opaque: use an internal order ID rather than a client-controlled JSON object.

Validate pre_checkout_query

from aiogram import Router
from aiogram.types import PreCheckoutQuery

router = Router()

@router.pre_checkout_query()
async def process_pre_checkout(query: PreCheckoutQuery):
    order_id = query.invoice_payload.removeprefix("course:")
    order = await payments.get(order_id)

    valid = bool(
        order
        and order.status == "pending"
        and order.user_id == query.from_user.id
        and order.currency == "XTR"
        and order.amount == query.total_amount
    )

    await query.answer(
        ok=valid,
        error_message=None if valid else "This order has expired. Create a new invoice.",
    )

Do not answer every query with ok=True. Check the user, amount, currency, status, and the order referenced by the payload.

Process successful_payment once

from aiogram import F
from aiogram.types import Message


@router.message(F.successful_payment)
async def process_successful_payment(message: Message):
    payment = message.successful_payment
    order_id = payment.invoice_payload.removeprefix("course:")

    order = await payments.mark_paid_once(
        order_id=order_id,
        user_id=message.from_user.id,
        amount=payment.total_amount,
        currency=payment.currency,
        charge_id=payment.telegram_payment_charge_id,
    )

    if order.just_paid:
        await courses.grant_access(order.user_id, order.product_slug)

    await message.answer("Payment confirmed. Your access is ready.")

mark_paid_once should run in a database transaction. A unique constraint on telegram_charge_id prevents duplicate delivery if Telegram retries an update or the bot restarts during processing.

Refund a Stars payment

Keep telegram_payment_charge_id; the Bot API needs it for a refund.

await bot.refund_star_payment(
    user_id=order.user_id,
    telegram_payment_charge_id=order.telegram_charge_id,
)

After Telegram confirms the refund, mark the order as refunded and recompute access. Do not revoke access if the user still has another active purchase or a free entitlement.

Bots selling digital goods also need a working /paysupport command and timely payment support.

Common mistakes

  • Trusting a price sent by a Mini App.
  • Approving pre-checkout without validating the order.
  • Delivering before successful_payment.
  • Losing the Telegram charge ID.
  • Processing the same payment more than once.
  • Putting access rules directly inside Telegram handlers.

For a first project, start with the free Aiogram 3 workshop. The complete Telegram bot course covers PostgreSQL, Stars, Mini Apps, testing, and Docker deployment.

For an existing commercial bot, describe the integration you need.

Need a custom Telegram bot?

I build bots, Mini Apps, payments and automation end to end.