все статьи
24 августа 2026 г.·11 мин

Telegram Stars в Aiogram 3: оплата, проверка и возвраты

Практическая интеграция Telegram Stars в бота на Python и Aiogram 3: invoice, pre_checkout_query, successful_payment, идемпотентность и возврат платежа.

Telegram StarsAiogramPythonПлатежиTelegram

Telegram Stars в Aiogram 3: оплата без уязвимостей

Telegram Stars нужны для продажи цифровых товаров и услуг внутри Telegram: доступа к курсу, подписки, файлов, функций Mini App. Для таких покупок бот использует валюту XTR, а сторонние платёжные провайдеры внутри Telegram применять нельзя. Это прямо указано в официальной документации Telegram.

Ниже не просто кнопка «Оплатить», а полный безопасный поток: локальный заказ, проверка суммы, защита от повторной выдачи и возврат.

Как устроен платёжный поток

Надёжная схема выглядит так:

  1. Backend создаёт заказ со статусом pending.
  2. Цена берётся из серверного каталога, а не из запроса клиента.
  3. Бот создаёт invoice в валюте XTR.
  4. На pre_checkout_query backend повторно проверяет заказ.
  5. После successful_payment сохраняется идентификатор платежа.
  6. Доступ выдаётся идемпотентно: повторный update ничего не выдаёт второй раз.

Клиенту нельзя разрешать передавать цену или Telegram ID покупателя. Он выбирает только товар, всё остальное определяет сервер.

Модель заказа

Для небольшого проекта достаточно такой структуры:

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
);

amount для Stars хранится целым числом. Если товар стоит 500 Stars, в заказе должно быть 500, без копеек и пересчёта через float.

Пример для Aiogram 3:

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-боты на Python",
        description="Бессрочный доступ к 12 урокам и четырём проектам",
        payload=f"course:{order_id}",
        currency="XTR",
        prices=[LabeledPrice(label="Доступ к курсу", amount=stars)],
    )

Для Stars не нужен токен внешнего провайдера. В payload передаётся непрозрачный ID локального заказа, а не JSON с ценой и пользователем.

Проверка pre_checkout_query

Telegram ждёт ответ на pre-checkout ограниченное время, поэтому обработчик должен быть коротким. Но отвечать ok=True без проверки нельзя.

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 "Заказ устарел. Создайте новый счёт.",
    )

Проверяются пользователь, валюта, сумма, статус и связь payload с заказом. Так подмена запроса на frontend не изменит стоимость товара.

Обработка successful_payment

Успешный платёж приходит как service message. Сначала фиксируем его в базе, затем выдаём доступ.

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("Оплата подтверждена. Доступ открыт.")

mark_paid_once должен выполняться в транзакции. Уникальный индекс на telegram_charge_id защищает от повторной обработки одного платежа после рестарта или повторной доставки update.

Возврат Stars

Сохраняйте telegram_payment_charge_id: он нужен для возврата через Bot API.

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

После успешного ответа пометьте заказ как refunded и пересчитайте доступ. Если пользователь купил один продукт дважды, возврат только одной покупки не должен автоматически закрывать вторую.

Telegram также требует от бота, продающего цифровые товары, обрабатывать команду /paysupport и помогать с платёжными вопросами.

Что чаще всего ломают

  • Доверяют сумме, присланной Mini App.
  • Безусловно подтверждают любой pre_checkout_query.
  • Выдают товар до successful_payment.
  • Не сохраняют telegram_payment_charge_id.
  • Повторно выдают товар при повторном update.
  • Смешивают бизнес-логику доступа с Telegram handler.

Handler должен только проверить входные данные и вызвать платёжный сервис. Каталог, заказы и выдача доступа остаются на backend.

Куда двигаться дальше

Для первого бота начните с бесплатного практикума по Aiogram 3. Полный поток с PostgreSQL, платежами, Mini App, тестами и Docker разобран в курсе по коммерческим Telegram-ботам.

Если нужна интеграция платежей в готовый проект, можно обсудить разработку.

Нужна разработка Telegram-бота?

Разрабатываю ботов, Mini Apps, платежи и автоматизацию под ключ.