Как подключить оплату через CryptoBot в бота на aiogram 3: Crypto Pay API с кодом

Приём криптовалюты в Telegram-боте через CryptoBot (Crypto Pay API) на aiogram 3: токен, создание счёта, проверка оплаты кнопкой и вебхук с проверкой подписи. Полный код.

8 мин чтения

CryptoBot (@CryptoBot) — популярный криптокошелёк внутри Telegram. Через его платёжный сервис Crypto Pay бот может выставлять счета в USDT, TON, BTC и других монетах: покупатель оплачивает их прямо в Telegram из своего кошелька CryptoBot, а деньги приходят на баланс вашего приложения.

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

Как устроена оплата через Crypto Pay

  1. Бот создаёт счёт методом createInvoice и получает ссылку на оплату.
  2. Пользователь переходит по ссылке и платит в @CryptoBot.
  3. Бот узнаёт об оплате одним из двух способов: пользователь нажимает «Я оплатил» и бот запрашивает статус счёта (getInvoices), или CryptoBot сам присылает уведомление на ваш сервер (вебхук).
  4. Счёт в статусе paid — бот выдаёт товар.

Шаг 1. Получите токен Crypto Pay

  1. Откройте @CryptoBot → Crypto Pay → Создать приложение.
  2. Задайте название — бот выдаст API-токен приложения.
  3. Сохраните токен в переменную окружения CRYPTO_PAY_TOKEN. Не публикуйте его: с ним можно управлять счетами вашего приложения.

Шаг 2. Установите aiogram

pip install -U aiogram

aiohttp, через который мы будем обращаться к API, ставится вместе с aiogram.

Шаг 3. Клиент Crypto Pay

Все запросы к Crypto Pay идут на https://pay.crypt.bot/api/<метод> с токеном в заголовке Crypto-Pay-API-Token. Сохраните небольшой клиент в файл cryptopay.py:

import aiohttp

API_URL = "https://pay.crypt.bot/api"  # тестовая сеть: https://testnet-pay.crypt.bot/api


class CryptoPay:
    # Минимальный клиент Crypto Pay API (@CryptoBot)

    def __init__(self, token: str, api_url: str = API_URL) -> None:
        self.token = token
        self.api_url = api_url

    async def call(self, method: str, **params) -> dict:
        headers = {"Crypto-Pay-API-Token": self.token}
        async with aiohttp.ClientSession() as session:
            async with session.post(f"{self.api_url}/{method}", json=params, headers=headers) as resp:
                data = await resp.json()
        if not data.get("ok"):
            raise RuntimeError(f"Crypto Pay: {data.get('error')}")
        return data["result"]

    async def create_invoice(self, amount: str, asset: str, description: str, payload: str) -> dict:
        return await self.call(
            "createInvoice",
            asset=asset,          # монета: USDT, TON, BTC...
            amount=amount,        # сумма строкой, например "5" или "2.5"
            description=description,
            payload=payload,      # ваша метка заказа, вернётся вместе со счётом
            expires_in=3600,      # счёт действует 1 час
        )

    async def get_invoice(self, invoice_id: int) -> dict | None:
        result = await self.call("getInvoices", invoice_ids=str(invoice_id))
        items = result.get("items", [])
        return items[0] if items else None

Шаг 4. Бот с оплатой и кнопкой проверки

Файл bot.py:

import asyncio
import os
from decimal import Decimal

from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import Command
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message

from cryptopay import CryptoPay

PRICE = Decimal("5")
ASSET = "USDT"

router = Router()
crypto = CryptoPay(os.environ["CRYPTO_PAY_TOKEN"])
delivered: set[int] = set()  # счета, по которым товар уже выдан (в реальном боте — база данных)


async def deliver(bot: Bot, invoice: dict) -> bool:
    # Выдаёт товар по оплаченному счёту. Вызывается и из кнопки, и из вебхука, поэтому выдача — строго один раз
    if invoice.get("status") != "paid" or invoice["invoice_id"] in delivered:
        return False
    if invoice.get("asset") != ASSET or Decimal(invoice["amount"]) != PRICE:
        return False
    delivered.add(invoice["invoice_id"])
    user_id = int(invoice["payload"])
    # Здесь выдайте товар: откройте доступ, отправьте файл, начислите баланс
    await bot.send_message(user_id, "Оплата получена! Доступ открыт ✅")
    return True


@router.message(Command("buy"))
async def buy(message: Message) -> None:
    invoice = await crypto.create_invoice(
        amount=str(PRICE), asset=ASSET,
        description="Доступ к курсу",
        payload=str(message.from_user.id),  # кому выдать товар после оплаты
    )
    keyboard = InlineKeyboardMarkup(inline_keyboard=[
        [InlineKeyboardButton(text=f"💳 Оплатить {PRICE} {ASSET}", url=invoice["bot_invoice_url"])],
        [InlineKeyboardButton(text="✅ Я оплатил", callback_data=f"check:{invoice['invoice_id']}")],
    ])
    await message.answer("Счёт на оплату действует 1 час:", reply_markup=keyboard)


@router.callback_query(F.data.startswith("check:"))
async def check(callback: CallbackQuery, bot: Bot) -> None:
    invoice_id = int(callback.data.split(":")[1])
    invoice = await crypto.get_invoice(invoice_id)
    # Счёт должен принадлежать тому, кто нажал кнопку
    if not invoice or invoice.get("payload") != str(callback.from_user.id):
        await callback.answer("Счёт не найден", show_alert=True)
        return
    if invoice["status"] == "expired":
        await callback.answer("Счёт просрочен. Создайте новый: /buy", show_alert=True)
        return
    if invoice["status"] != "paid":
        await callback.answer("Оплата пока не поступила", show_alert=True)
        return
    await deliver(bot, invoice)
    await callback.message.edit_text("Счёт оплачен ✅")
    await callback.answer()


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


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

Запуск:

export BOT_TOKEN="токен_от_BotFather"
export CRYPTO_PAY_TOKEN="токен_Crypto_Pay"
python bot.py

Отправьте боту /buy, оплатите счёт в @CryptoBot и нажмите «Я оплатил».

Шаг 5. Мгновенные уведомления через вебхук

Кнопка «Я оплатил» работает всегда, но пользователь может её не нажать. Чтобы бот узнавал об оплате сам, включите вебхук: @CryptoBot → Crypto Pay → ваше приложение → Webhooks → укажите адрес вида https://ваш-домен/webhooks/cryptobot. Нужен сервер с доменом и HTTPS.

Каждое уведомление CryptoBot подписывает: в заголовке crypto-pay-api-signature лежит HMAC-SHA256 тела запроса, где ключ — SHA-256 от вашего токена. Уведомление без верной подписи может прислать кто угодно, поэтому такие запросы отклоняем.

Добавьте в bot.py обработчик вебхука и замените функцию main — веб-сервер для уведомлений запустится вместе с ботом. Новые импорты перенесите в начало файла:

import hashlib
import hmac
import json

from aiohttp import web

TOKEN = os.environ["CRYPTO_PAY_TOKEN"]


def check_signature(body: bytes, signature: str) -> bool:
    secret = hashlib.sha256(TOKEN.encode()).digest()  # ключ подписи — SHA-256 от токена
    expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


async def cryptobot_webhook(request: web.Request) -> web.Response:
    body = await request.read()
    if not check_signature(body, request.headers.get("crypto-pay-api-signature", "")):
        return web.Response(status=401)
    update = json.loads(body)
    if update.get("update_type") == "invoice_paid":
        await deliver(request.app["bot"], update["payload"])  # payload здесь — сам счёт
    return web.Response(text="ok")  # отвечаем 200, иначе CryptoBot повторит уведомление


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

    app = web.Application()
    app["bot"] = bot
    app.router.add_post("/webhooks/cryptobot", cryptobot_webhook)
    runner = web.AppRunner(app)
    await runner.setup()
    await web.TCPSite(runner, "127.0.0.1", 8080).start()  # снаружи — через nginx с HTTPS

    try:
        await dp.start_polling(bot)
    finally:
        await runner.cleanup()

Веб-сервер слушает только 127.0.0.1:8080, а наружу его открывает nginx (или Caddy) с HTTPS-сертификатом — проксируйте путь /webhooks/cryptobot на этот адрес. Функция deliver общая для кнопки и вебхука и не выдаст товар дважды.

Оплата с ценой в рублях или долларах

Если цены в боте указаны в фиатной валюте, создайте счёт с currency_type="fiat": CryptoBot сам пересчитает сумму по курсу, а пользователь выберет монету для оплаты.

invoice = await crypto.call(
    "createInvoice",
    currency_type="fiat",
    fiat="RUB",
    amount="500",
    accepted_assets="USDT,TON",
    description="Доступ к курсу",
    payload=str(message.from_user.id),
)

В таком счёте проверяйте поля fiat и amount — сумму в рублях, а не в монете.

Безопасность: что проверить перед запуском

  • Токен — только в переменных окружения или .env, никогда в коде и репозитории.
  • Статус paid — единственный признак оплаты. Не выдавайте товар по нажатию кнопки без проверки статуса через API.
  • Сумма и монета — сверяйте с заказом: так никто не подсунет оплаченный счёт на меньшую сумму.
  • Принадлежность счёта — payload должен совпадать с покупателем или номером его заказа.
  • Однократная выдача — храните обработанные invoice_id в базе данных. Множество в памяти из примера очищается при перезапуске бота.
  • Подпись вебхука — проверяйте всегда, сравнение — через hmac.compare_digest.

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

  • UNAUTHORIZED — неверный токен или токен тестовой сети отправлен на основной адрес API (и наоборот).
  • Сумма числом. amount передавайте строкой: "5", "2.5".
  • Вебхук не приходит. Адрес должен быть доступен из интернета по HTTPS, а сервер — отвечать 200.
  • Один ClientSession на запрос. Для учебного примера это нормально, а под нагрузкой создайте одну сессию при старте бота и закрывайте её при остановке.

Другие способы оплаты — Telegram Stars и xRocket; сравнение всех вариантов — в статье «Как принимать оплату в Telegram-боте».

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

Нужна ли верификация, чтобы принимать оплату через CryptoBot?

Для создания приложения в Crypto Pay достаточно аккаунта в @CryptoBot. Актуальные требования и лимиты смотрите в самом сервисе — они могут меняться.

В каких монетах можно выставить счёт?

В тех, что поддерживает CryptoBot: USDT, TON, BTC, ETH и другие. Список доступных монет возвращает метод getCurrencies.

Как протестировать оплату бесплатно?

Через тестовую сеть: бот @CryptoTestnetBot и адрес https://testnet-pay.crypt.bot/api. Там свои токен и ненастоящие монеты.

Можно ли обойтись без вебхука?

Да. Кнопки «Я оплатил» достаточно для запуска, а вебхук добавляют, чтобы бот выдавал товар сразу, даже если пользователь забыл нажать кнопку.

Куда приходят деньги?

На баланс вашего приложения в @CryptoBot. Оттуда их можно вывести на свой кошелёк.

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

⭐️ Auto Buy Stars WEB APPБОТ

⭐️ Auto Buy Stars WEB APP

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

240,00 $≈ 19 333,75 ₽