Как подключить оплату Telegram Stars в бота на aiogram 3: пошаговая инструкция с кодом

Приём оплаты звёздами Telegram в боте на aiogram 3: счёт, pre_checkout_query, успешный платёж, возвраты, ссылка на оплату и вывод звёзд. Полный рабочий код с пояснениями.

7 мин чтения

Telegram Stars — встроенная валюта Telegram. Пользователь покупает звёзды прямо в приложении, а потом тратит их в ботах в два касания, без ввода карты и перехода на сторонние сайты. Для владельца бота это самый простой способ принимать оплату: не нужен платёжный провайдер, договор и токен оплаты — всё работает через обычный Bot API.

По правилам Telegram цифровые товары и услуги в ботах — доступы, подписки, файлы, внутриигровые предметы — нужно продавать именно за Stars. Ниже — рабочий бот на aiogram 3, который выставляет счёт, принимает оплату, выдаёт товар и умеет делать возвраты.

Как проходит оплата звёздами

  1. Бот отправляет пользователю счёт (invoice) с валютой XTR — так в Bot API обозначаются звёзды.
  2. Пользователь нажимает «Оплатить», и Telegram присылает боту pre_checkout_query — последний запрос перед списанием. Бот должен ответить на него в течение 10 секунд: подтвердить или отклонить.
  3. После подтверждения Telegram списывает звёзды и присылает сообщение с successful_payment. Только в этот момент оплата считается завершённой — здесь и выдаётся товар.

Что понадобится

  • Бот, созданный через @BotFather, и его токен. Отдельно подключать платежи в BotFather для звёзд не нужно.
  • Python 3.10 или новее.
  • aiogram 3 — ставится одной командой:
pip install -U aiogram

Токен бота не пишите в код — передавайте через переменные окружения или файл .env, который не попадает в репозиторий. В примере ниже бот читает BOT_TOKEN и ADMIN_ID — ваш Telegram ID, которому разрешены возвраты.

Полный код бота

Сохраните код в файл bot.py:

import asyncio
import logging
import os

from aiogram import Bot, Dispatcher, F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import Command, CommandObject, CommandStart
from aiogram.types import LabeledPrice, Message, PreCheckoutQuery

ADMIN_ID = int(os.environ["ADMIN_ID"])  # ваш Telegram ID — только ему доступен /refund

# Товар: название, описание, цена в звёздах (целое число) и метка заказа
PRODUCT_TITLE = "Доступ к курсу"
PRODUCT_DESCRIPTION = "Полный доступ ко всем урокам курса навсегда"
PRODUCT_PAYLOAD = "course_full"
PRICE_STARS = 50

router = Router()


@router.message(CommandStart())
async def start(message: Message) -> None:
    await message.answer(f"Привет! Курс стоит {PRICE_STARS} ⭐. Купить: /buy")


@router.message(Command("buy"))
async def buy(message: Message) -> None:
    await message.answer_invoice(
        title=PRODUCT_TITLE,
        description=PRODUCT_DESCRIPTION,
        payload=PRODUCT_PAYLOAD,  # вернётся в pre_checkout_query и successful_payment
        currency="XTR",           # XTR — Telegram Stars, provider_token не нужен
        prices=[LabeledPrice(label=PRODUCT_TITLE, amount=PRICE_STARS)],
    )


@router.pre_checkout_query()
async def pre_checkout(query: PreCheckoutQuery) -> None:
    # Последняя проверка перед списанием: товар существует и в наличии.
    # Ответить нужно за 10 секунд, иначе Telegram отменит оплату.
    if query.invoice_payload != PRODUCT_PAYLOAD:
        await query.answer(ok=False, error_message="Товар не найден. Создайте счёт заново: /buy")
        return
    await query.answer(ok=True)


@router.message(F.successful_payment)
async def paid(message: Message) -> None:
    payment = message.successful_payment
    if payment.invoice_payload != PRODUCT_PAYLOAD or payment.total_amount != PRICE_STARS:
        logging.warning("Неожиданный платёж: %s", payment.invoice_payload)
        return
    # charge_id сохраните в базе вместе с заказом — без него не сделать возврат
    logging.info("Оплата %s ⭐ от %s, charge_id=%s",
                 payment.total_amount, message.from_user.id, payment.telegram_payment_charge_id)
    # Здесь выдайте товар: откройте доступ, отправьте файл, начислите баланс
    await message.answer("Спасибо за покупку! Доступ к курсу открыт ✅")


@router.message(Command("refund"), F.from_user.id == ADMIN_ID)
async def refund(message: Message, command: CommandObject, bot: Bot) -> None:
    # /refund <user_id> <charge_id> — вернуть звёзды покупателю
    args = (command.args or "").split()
    if len(args) != 2 or not args[0].isdigit():
        await message.answer("Формат: /refund <user_id> <charge_id>")
        return
    try:
        await bot.refund_star_payment(user_id=int(args[0]), telegram_payment_charge_id=args[1])
    except TelegramBadRequest as e:
        await message.answer(f"Возврат не прошёл: {e.message}")
        return
    await message.answer("Звёзды возвращены покупателю")


@router.message(Command("paysupport"))
async def pay_support(message: Message) -> None:
    await message.answer("Вопросы по оплате: напишите @your_support — ответим в течение дня.")


async def main() -> None:
    logging.basicConfig(level=logging.INFO)
    bot = Bot(token=os.environ["BOT_TOKEN"])
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

Запуск в Linux или macOS:

export BOT_TOKEN="токен_от_BotFather"
export ADMIN_ID="ваш_telegram_id"
python bot.py

Отправьте боту /buy — придёт счёт с кнопкой «Оплатить 50 ⭐».

Разбор кода

Счёт: answer_invoice

  • currency="XTR" — оплата звёздами. Для звёзд параметр provider_token не передаётся.
  • prices — список позиций. Для Stars в нём должна быть ровно одна позиция, а amount — целое число звёзд (не копейки и не центы).
  • payload — ваша внутренняя метка заказа. Пользователь её не видит, а Telegram возвращает её после оплаты — по ней вы понимаете, что именно оплачено. В настоящем магазине сюда пишут номер заказа из базы данных.

pre_checkout_query

Здесь проверяют, можно ли ещё продать товар: не закончился ли он, не изменилась ли цена, не оплачен ли заказ раньше. Если что-то не так — отвечайте ok=False с понятным текстом ошибки, и Telegram покажет его пользователю.

successful_payment

Главный обработчик: звёзды уже списаны, нужно выдать товар. Обязательно:

  • сверяйте payload и сумму с заказом в базе;
  • сохраняйте telegram_payment_charge_id — это номер платежа, он нужен для возврата и для разбора споров;
  • выдавайте товар один раз — отмечайте заказ оплаченным в базе.

Возврат звёзд

Метод refund_star_payment возвращает звёзды покупателю целиком. В примере команда /refund доступна только администратору — фильтр F.from_user.id == ADMIN_ID. Не давайте пользователям делать возврат самостоятельно: иначе можно получить товар и сразу вернуть деньги.

После возврата не забудьте забрать выданное: закрыть доступ или аннулировать покупку в базе.

Ссылка на оплату вместо сообщения

Если счёт нужно разместить в канале, на кнопке или в мини-приложении (Web App), создайте ссылку на оплату:

link = await bot.create_invoice_link(
    title="Доступ к курсу",
    description="Полный доступ ко всем урокам курса",
    payload="course_full",
    currency="XTR",
    prices=[LabeledPrice(label="Доступ к курсу", amount=50)],
)

По такой ссылке оплата проходит так же: бот получает pre_checkout_query и successful_payment.

Команда /paysupport

Telegram требует, чтобы у бота, который принимает оплату, была понятная поддержка по платежам — для этого и нужна команда /paysupport. Добавьте её в меню через @BotFather → Edit Bot → Edit Commands, вместе с /buy.

Как вывести заработанные звёзды

Звёзды накапливаются на балансе бота. Посмотреть их и вывести можно в профиле бота в Telegram (раздел с балансом звёзд) — вывод идёт через Fragment в TON. Звёзды становятся доступны к выводу не сразу, а после периода удержания; актуальные сроки и минимальную сумму смотрите в самом Telegram — условия периодически меняются.

Частые ошибки

  • Бот выдаёт товар в pre_checkout_query. Оплата ещё не прошла — переносите выдачу в successful_payment.
  • Обработчик pre_checkout_query не отвечает или работает дольше 10 секунд. Оплата отменяется с ошибкой у пользователя. Не делайте здесь долгих запросов.
  • Несколько позиций в prices. Для XTR допускается только одна.
  • Нет записи charge_id. Без него невозможно сделать возврат.
  • Токен в коде. Храните его в .env и не публикуйте в репозитории.

Приём оплаты другими способами — криптовалютой через CryptoBot или xRocket — разобран в отдельных инструкциях, а обзор всех вариантов — в статье «Как принимать оплату в Telegram-боте».

Частые вопросы

Нужен ли provider_token для оплаты звёздами?

Нет. Для валюты XTR платёжный провайдер не нужен, параметр provider_token не передаётся.

Как протестировать оплату Stars, не тратя деньги?

Купите немного звёзд, поставьте цену 1 ⭐, оплатите и сделайте возврат командой /refund — звёзды вернутся на ваш баланс.

Можно ли принимать звёзды за физические товары?

Звёзды предназначены для цифровых товаров и услуг. Для физических товаров используйте платёжных провайдеров или другие способы оплаты.

Как вывести звёзды из бота?

Через баланс звёзд в профиле бота — вывод идёт через Fragment в TON после периода удержания. Актуальные условия смотрите в Telegram.

Почему оплата отменяется сразу после нажатия «Оплатить»?

Бот не ответил на pre_checkout_query за 10 секунд или ответил ok=False. Проверьте, что обработчик подключён и работает быстро.

Готовые решения по теме

⭐️ Auto Buy Stars WEB APPБОТ

⭐️ Auto Buy Stars WEB APP

Бот для покупки Stars/Premium/Пополнением Steam в автоматическом режиме с Fragment Установка производится покупателем

240,00 $≈ 19 333,75 ₽