$ sudo teach IT
Модуль 5 · Transfers — переводы между пользователями

Урок 5.1 — Создание переводов

Научимся отправлять переводы пользователям Crypto Pay. Разберём параметры метода transfer(), идемпотентность через spend_id и типы данных Transfers.

Переводы (Transfers) — это возможность отправлять средства напрямую пользователям @CryptoBot через API. В отличие от чеков, где пользователь сам активирует получение, переводы отправляются принудительно: ты указываешь user_id получателя, и средства зачисляются ему на баланс. В этом уроке мы разберём, как настроить переводы, создать первый перевод и обеспечить идемпотентность.

🔓

Включение переводов в настройках приложения

Прежде чем использовать переводы, их нужно включить в настройках твоего приложения в @CryptoBot или @CryptoTestnetBot. По умолчанию переводы отключены для всех новых приложений.

🔧 Как включить переводы:

  1. Открой чат с @CryptoBot (или @CryptoTestnetBot для тестов)
  2. Перейди в раздел Crypto Pay
  3. Выбери своё приложение (или создай новое)
  4. Перейди в Security → Transfers
  5. Включи опцию Enable Transfers
  6. Подтверди изменения

‼️ Важно: переводы не включаются по умолчанию

Если ты попытаешься вызвать transfer() с выключенными переводами, API вернёт ошибку. Убедись, что переводы включены в настройках приложения перед использованием. Эта опция находится в разделе Security — она выключена по умолчанию для безопасности.

💸

Метод transfer() — отправка перевода

Метод transfer() отправляет средства указанному пользователю @CryptoBot. Для получателя это выглядит как мгновенное зачисление на баланс в боте. Твоё приложение должно иметь достаточный баланс для отправки.

Python · Сигнатура метода
async def transfer(
    self,
    user_id: int,
    asset: str,
    amount: float | str,
    spend_id: str | None = None,
    comment: str | None = None,
    disable_send_notification: bool = False,
) -> Transfer

Только user_id, asset и amount обязательны. Остальные параметры опциональны.

Python · Таблица параметров transfer()
Параметр Тип Обязательный Описание
user_id int ✅ Да Telegram ID пользователя-получателя в системе @CryptoBot. Пользователь должен хотя бы раз запустить @CryptoBot.
asset str ✅ Да Актив для перевода (например "USDT", "TON"). Должен быть на балансе приложения.
amount float | str ✅ Да Сумма перевода. Для криптовалют можно передавать строку (например "0.000001").
spend_id str | None Нет Уникальный ID для идемпотентности. Предотвращает повторную отправку. Генерируется клиентом (рандомная UTF-8 строка).
comment str | None Нет Комментарий к переводу. Получатель увидит его в @CryptoBot. Максимум 1024 символа.
disable_send_notification bool Нет Отключить уведомление получателя в Telegram (по умолчанию False — уведомление отправляется).
Python · Простейший перевод
from aiosend import CryptoPay

cp = CryptoPay(token="YOUR_TOKEN")

# Простейший перевод — только обязательные параметры
transfer = await cp.transfer(
    user_id=123456789,
    asset="USDT",
    amount=10,
)

print(f"Перевод #{transfer.transfer_id}")
print(f"Получатель: {transfer.user_id}")
print(f"Сумма: {transfer.amount} {transfer.asset}")
print(f"Статус: {transfer.status}")
print(f"ID списания: {transfer.spend_id}")

💡 Как получить user_id пользователя?

Чтобы отправить перевод, нужно знать Telegram ID пользователя (user_id). Его можно получить: из оплаченного инвойса (поле invoice.payer_id), из активированного чека (check.activated_by), через вашего Telegram-бота (объект message.from_user.id), или через вебхук от Crypto Pay. Пользователь должен хотя бы раз запустить @CryptoBot.

🆔

Идемпотентность: параметр spend_id

Параметр spend_id — это механизм идемпотентности. Он гарантирует, что один и тот же перевод не будет выполнен дважды. Если по какой-то причине ты отправишь запрос с тем же spend_id, который уже был использован, API вернёт существующий объект Transfer вместо создания нового.

Это критически важно для финансовых операций: представь, что клиент отправил запрос на перевод, но из-за сетевой ошибки не получил ответ. Если он повторит запрос без spend_id — средства будут списаны повторно. С spend_id — API вернёт результат первого перевода.

Python · Генерация spend_id
import uuid
import secrets

# Вариант 1: UUID4 — стандартный способ
spend_id = str(uuid.uuid4())
print(f"spend_id: {spend_id}")
# → "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

# Вариант 2: secrets — криптостойкая случайная строка
spend_id = secrets.token_hex(16)
print(f"spend_id: {spend_id}")
# → "d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9"

# Вариант 3: комбинация user_id + timestamp + random
import hashlib
import time

raw = f"user_{123456789}_time_{int(time.time())}_rand_{secrets.randbits(32)}"
spend_id = hashlib.sha256(raw.encode()).hexdigest()
print(f"spend_id: {spend_id}")
# → "a1b2c3d4..." (64 символа hex)

Требования к spend_id:

  • Уникальная строка в кодировке UTF-8
  • Генерируется на стороне клиента (ты сам отвечаешь за уникальность)
  • После использования spend_id сохраняется в истории — повторная отправка с тем же ID вернёт существующий Transfer
  • Рекомендуется использовать UUID4 или криптостойкие случайные строки
Python · Перевод с spend_id (идемпотентный)
import uuid
from aiosend import CryptoPay

async def send_money(cp: CryptoPay, user_id: int, amount: float):
    # Генерируем уникальный spend_id
    spend_id = str(uuid.uuid4())

    transfer = await cp.transfer(
        user_id=user_id,
        asset="USDT",
        amount=amount,
        spend_id=spend_id,
        comment="Спасибо за покупку!",
    )
    print(f"Перевод выполнен: #{transfer.transfer_id}")
    return transfer

# Если запрос повторится с тем же spend_id —
# API вернёт существующий перевод, не списывая средства повторно
async def safe_send(cp: CryptoPay, user_id: int, amount: float):
    spend_id = "fixed-spend-id-for-order-12345"

    # Первый вызов — создаст перевод
    t1 = await cp.transfer(user_id=user_id, asset="USDT",
                          amount=amount, spend_id=spend_id)
    print(f"Первый раз: #{t1.transfer_id}")

    # Второй вызов с тем же spend_id — вернёт тот же перевод
    t2 = await cp.transfer(user_id=user_id, asset="USDT",
                          amount=amount, spend_id=spend_id)
    print(f"Второй раз: #{t2.transfer_id} (тот же!)")
    assert t1.transfer_id == t2.transfer_id  # True!

💡 Практический совет по spend_id

В реальном проекте spend_id должен быть привязан к бизнес-логике. Например: f"order_{order_id}" или f"refund_{payment_id}". Так ты гарантируешь, что возврат средств за конкретный заказ не сработает дважды. Храни spend_id в своей базе данных вместе с ID заказа.

📄

Тип Transfer — все поля объекта

Метод transfer() возвращает объект Transfer (Pydantic-модель). Вот все его поля:

Поле Тип Описание
transfer_id int Уникальный ID перевода в системе Crypto Pay
user_id int Telegram ID получателя
asset str Актив перевода (например "USDT", "TON")
amount str Сумма перевода (строка для сохранения точности)
status str Статус перевода: обычно "completed"
completed_at str | None Дата завершения перевода в формате ISO 8601
comment str | None Комментарий к переводу (если указан)
spend_id str | None Уникальный ID идемпотентности (если указан при создании)
created_at str Дата создания перевода
Python · Доступ к полям Transfer
transfer = await cp.transfer(
    user_id=123456789,
    asset="USDT",
    amount=25.50,
    spend_id=str(uuid.uuid4()),
    comment="Бонус за регистрацию",
)

print(f"ID перевода: {transfer.transfer_id}")
print(f"Получатель: {transfer.user_id}")
print(f"Сумма: {transfer.amount} {transfer.asset}")
print(f"Статус: {transfer.status}")
print(f"Дата: {transfer.completed_at}")
print(f"Комментарий: {transfer.comment}")
print(f"spend_id: {transfer.spend_id}")

# Проверка на успешность
if transfer.status == "completed":
    print("✅ Перевод успешно выполнен")
💬

comment и disable_send_notification

Эти два параметра управляют тем, как получатель видит перевод.

comment

  • Текстовый комментарий (до 1024 симв.)
  • Получатель видит его в @CryptoBot
  • Помогает объяснить назначение перевода
  • Пример: "Зарплата за май", "Кэшбек 5%"

disable_send_notification

  • По умолчанию False — уведомление отправляется
  • True — получатель НЕ получит уведомление в Telegram
  • Средства всё равно зачислятся на баланс
  • Полезно для массовых или фоновых выплат
Python · Перевод с комментарием и без уведомления
# Перевод с комментарием — получатель увидит "Бонус!"
transfer = await cp.transfer(
    user_id=123456789,
    asset="USDT",
    amount=5,
    comment="Бонус за подписку! 🎉",
)

# Перевод без уведомления — тихая выплата
transfer = await cp.transfer(
    user_id=987654321,
    asset="TON",
    amount=10,
    disable_send_notification=True,
    comment="Автоматическая выплата",
)

# Перевод с комментарием и без уведомления
transfer = await cp.transfer(
    user_id=555555555,
    asset="USDT",
    amount=100,
    spend_id=str(uuid.uuid4()),
    comment="Возврат средств за заказ #12345",
    disable_send_notification=False,  # пусть знает
)

⚠️ Когда отключать уведомления?

Отключай уведомления (disable_send_notification=True) только когда получатель не должен отвлекаться на каждую выплату. Например: массовые выплаты партнёрам, регулярные начисления, фоновые операции. Для разовых переводов (возвраты, бонусы) лучше оставлять уведомления включёнными — это повышает доверие пользователей.

⚖️

Минимальные и максимальные суммы переводов

Каждый актив имеет ограничения по минимальной и максимальной сумме перевода. Точные значения можно получить через get_currencies(), но вот примерные ориентиры:

Актив Мин. сумма Макс. сумма (~USD) Decimals
USDT 1 USDT ~25 000 USD 2
TON 0.1 TON ~25 000 USD 9
BTC 0.0001 BTC ~25 000 USD 8
ETH 0.001 ETH ~25 000 USD 8
USDC 1 USDC ~25 000 USD 2
SEND 10 SEND ~25 000 USD 9

Максимальная сумма перевода ограничена ~25 000 USD в эквиваленте для всех активов. Если нужно отправить больше — отправляй частями.

Python · Проверка лимитов перед переводом
from aiosend import CryptoPay

async def get_asset_limits(cp: CryptoPay, asset: str) -> tuple:
    """Возвращает (min_amount, max_amount) для актива."""
    currencies = await cp.get_currencies()
    for currency in currencies:
        if currency.code == asset:
            return (currency.min_amount, currency.max_amount)
    return (None, None)

async def safe_transfer(cp: CryptoPay, user_id: int, asset: str, amount: float):
    min_amt, max_amt = await get_asset_limits(cp, asset)

    if min_amt and float(amount) < float(min_amt):
        print(f"❌ Сумма {amount} меньше минимума {min_amt} для {asset}")
        return None

    if max_amt and float(amount) > float(max_amt):
        print(f"❌ Сумма {amount} превышает максимум {max_amt} для {asset}")
        return None

    # Все проверки пройдены — выполняем перевод
    transfer = await cp.transfer(
        user_id=user_id,
        asset=asset,
        amount=amount,
        spend_id=str(uuid.uuid4()),
    )
    return transfer
👤

Получатель: требование к пользователю

Важное условие: пользователь, которому ты отправляешь перевод, должен хотя бы один раз запустить @CryptoBot (или @CryptoTestnetBot для тестовой сети). Без этого API не сможет зачислить средства.

Python · Проверка возможности перевода
from aiosend import CryptoPay
from aiosend.exceptions import APIError

async def try_transfer(cp: CryptoPay, user_id: int, asset: str, amount: float):
    try:
        transfer = await cp.transfer(
            user_id=user_id,
            asset=asset,
            amount=amount,
            spend_id=str(uuid.uuid4()),
            comment="Проверка перевода",
        )
        print(f"✅ Перевод успешен: #{transfer.transfer_id}")
        return transfer

    except APIError as e:
        if "USER_NOT_FOUND" in str(e):
            print(f"❌ Пользователь {user_id} не найден в @CryptoBot.")
            print("   Убедитесь, что пользователь запустил @CryptoBot")
        elif "INSUFFICIENT_BALANCE" in str(e):
            print(f"❌ Недостаточно средств на балансе приложения")
        else:
            print(f"❌ Ошибка API: {e}")
        return None

⚠️ Что если пользователь не в @CryptoBot?

API вернёт ошибку с текстом, указывающим, что пользователь не найден. В этом случае ты можешь: попросить пользователя запустить @CryptoBot и повторить попытку, или использовать альтернативный способ отправки (например, чек).

🧩

Практические примеры

Рассмотрим несколько реальных сценариев отправки переводов.

Python · Пример 1: Отправка бонуса пользователю
import uuid
from aiosend import CryptoPay

async def send_bonus(cp: CryptoPay, user_id: int, amount: float, reason: str):
    """Отправляет бонус пользователю."""
    spend_id = f"bonus_{user_id}_{uuid.uuid4().hex[:8]}"

    transfer = await cp.transfer(
        user_id=user_id,
        asset="USDT",
        amount=amount,
        spend_id=spend_id,
        comment=f"🎁 Бонус: {reason}",
    )
    print(f"✅ Бонус отправлен: #{transfer.transfer_id}")
    print(f"   Получатель: {transfer.user_id}")
    print(f"   Сумма: {transfer.amount} {transfer.asset}")
    print(f"   Комментарий: {transfer.comment}")
    return transfer
Python · Пример 2: Массовые выплаты
import uuid
import asyncio
from aiosend import CryptoPay

async def batch_payouts(cp: CryptoPay, payouts: list[dict]):
    """Массовые выплаты пользователям.
    payouts = [
        {"user_id": 123, "amount": 10, "asset": "USDT"},
        {"user_id": 456, "amount": 5, "asset": "USDT"},
    ]
    """
    results = []
    for payout in payouts:
        try:
            transfer = await cp.transfer(
                user_id=payout["user_id"],
                asset=payout.get("asset", "USDT"),
                amount=payout["amount"],
                spend_id=str(uuid.uuid4()),
                comment="Массовая выплата",
                disable_send_notification=True,
            )
            results.append(transfer)
            print(f"✅ {payout['user_id']}: #{transfer.transfer_id}")
            await asyncio.sleep(0.1)  # защита от rate limit
        except Exception as e:
            print(f"❌ {payout['user_id']}: {e}")
            results.append(None)
    return results
Python · Пример 3: Возврат средств (refund)
async def refund(cp: CryptoPay, user_id: int, amount: float, order_id: str):
    """Возврат средств за заказ."""
    spend_id = f"refund_{order_id}"

    transfer = await cp.transfer(
        user_id=user_id,
        asset="USDT",
        amount=amount,
        spend_id=spend_id,
        comment=f"Возврат за заказ #{order_id}",
    )
    print(f"✅ Возврат оформлен: #{transfer.transfer_id}")
    print(f"   ID заказа: {order_id}")
    print(f"   Сумма: {transfer.amount} {transfer.asset}")
    return transfer

# Идемпотентность: если refund вызвали дважды с тем же spend_id
# — второй раз вернёт тот же Transfer
# t1 = await refund(cp, 123, 10, "ORD-001")
# t2 = await refund(cp, 123, 10, "ORD-001")
# assert t1.transfer_id == t2.transfer_id  # OK!
⚠️

Граничные случаи и ограничения

Недостаточный баланс

Если на балансе приложения недостаточно средств, API вернёт ошибку. Всегда проверяй баланс через get_balance() перед отправкой, особенно при массовых выплатах. Библиотека aiosend выбрасывает исключение APIError в этом случае.

Пользователь не в @CryptoBot

Если получатель никогда не запускал @CryptoBot, перевод не сработает. API вернёт ошибку. Рекомендуется предварительно проверять, что пользователь существует в системе Crypto Pay (через вебхуки или историю взаимодействий).

Rate limiting

Crypto Pay API имеет ограничение на количество запросов в минуту. При массовых отправках используй задержки (asyncio.sleep) между запросами, чтобы избежать ошибки TooManyRequestsError.

Комментарий слишком длинный

Максимальная длина комментария — 1024 символа. Если передать больше — API вернёт ошибку MethodValuesError. Обрезай комментарий перед отправкой при необходимости.

📌

Что важно запомнить

1️⃣
Включи переводы в настройках приложения: Crypto Pay → Security → Transfers. По умолчанию выключено.
2️⃣
transfer() — обязательные параметры: user_id, asset, amount. Опциональные: spend_id, comment, disable_send_notification.
3️⃣
spend_id — идемпотентность. Генерируй на клиенте (UUID4, secrets, бизнес-ключ). Предотвращает двойное списание.
4️⃣
Получатель должен хотя бы раз запустить @CryptoBot. Иначе — ошибка API.
5️⃣
Лимиты — минимальная сумма зависит от актива, максимальная ~25 000 USD. Проверяй через get_currencies().
6️⃣
comment — до 1024 символов. Получатель видит его. disable_send_notification — отключает уведомление.
7️⃣
Transfer — объект с полями: transfer_id, user_id, asset, amount, status, completed_at, comment, spend_id, created_at.
🎯

Практическое задание

Задание: Создай сервис отправки переводов

Напиши класс TransferService, который предоставляет удобный интерфейс для отправки переводов. Класс должен:

  • Принимать CryptoPay в конструкторе
  • Иметь метод send(user_id, asset, amount, comment=None) — базовый перевод с автоматическим spend_id
  • Иметь метод send_batch(payouts: list[dict]) — массовые выплаты с задержками
  • Иметь метод refund(user_id, amount, order_id) — возврат с идемпотентным spend_id на основе order_id
  • Проверять баланс перед отправкой (метод check_balance(asset, amount))
  • Вести лог всех операций

Подсказка:

Python · Шаблон решения
import uuid
import asyncio
from aiosend import CryptoPay
from aiosend.exceptions import APIError

class TransferService:
    def __init__(self, cp: CryptoPay):
        self.cp = cp

    async def check_balance(self, asset: str, amount: float) -> bool:
        balances = await self.cp.get_balances()
        for bal in balances:
            if bal.code == asset:
                available = float(bal.available)
                return available >= amount
        return False

    async def send(self, user_id: int, asset: str, amount: float,
                   comment: str | None = None) -> Transfer | None:
        if not await self.check_balance(asset, amount):
            print(f"❌ Недостаточно {asset} на балансе")
            return None
        try:
            transfer = await self.cp.transfer(
                user_id=user_id,
                asset=asset,
                amount=amount,
                spend_id=str(uuid.uuid4()),
                comment=comment,
            )
            print(f"✅ Перевод #{transfer.transfer_id}: {amount} {asset}")
            return transfer
        except APIError as e:
            print(f"❌ Ошибка перевода: {e}")
            return None

    async def refund(self, user_id: int, amount: float, order_id: str):
        spend_id = f"refund_{order_id}"
        return await self.cp.transfer(
            user_id=user_id, asset="USDT",
            amount=amount, spend_id=spend_id,
            comment=f"Refund #{order_id}",
        )

    async def send_batch(self, payouts: list[dict]) -> list:
        results = []
        for p in payouts:
            t = await self.send(p["user_id"], p.get("asset", "USDT"),
                                p["amount"], p.get("comment"))
            results.append(t)
            await asyncio.sleep(0.1)
        return results


async def main():
    cp = CryptoPay(token="YOUR_TOKEN")
    svc = TransferService(cp)

    await svc.send(user_id=123456789, asset="USDT", amount=10,
                   comment="Подарок!")

    # mass_payouts = [
    #     {"user_id": 111, "amount": 5},
    #     {"user_id": 222, "amount": 10},
    # ]
    # await svc.send_batch(mass_payouts)


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

Синхронный вариант: SyncCryptoPay.transfer()

Если ты работаешь в синхронном окружении (скрипты, Flask, простые утилиты), используй SyncCryptoPay. Он предоставляет те же методы, включая transfer() и get_transfers(), но без await.

Python · Синхронный перевод
from aiosend import SyncCryptoPay

cp = SyncCryptoPay(token="YOUR_TOKEN")

# Синхронный перевод — без await!
transfer = cp.transfer(
    user_id=123456789,
    asset="USDT",
    amount=10,
    comment="Синхронный перевод",
)

print(f"Перевод #{transfer.transfer_id}: {transfer.amount} {transfer.asset}")
print(f"Статус: {transfer.status}")

# Синхронное получение истории
transfers = cp.get_transfers(asset="USDT")
print(f"Найдено переводов: {len(transfers)}")

# Все параметры те же, что и в асинхронной версии
transfer = cp.transfer(
    user_id=987654321,
    asset="TON",
    amount=5,
    spend_id=str(uuid.uuid4()),
    comment="Бонус",
    disable_send_notification=True,
)

Синхронный vs асинхронный подход

Выбор между CryptoPay и SyncCryptoPay зависит от твоего проекта. Для ботов (aiogram, Pyrogram) и веб-фреймворков (FastAPI, aiohttp) используй асинхронный вариант. Для простых скриптов, Flask или консольных утилит — синхронный. Функциональность идентична, разница только в наличии await.

⚖️

Сравнение: Чеки (Checks) vs Переводы (Transfers)

Важно понимать разницу между двумя способами отправки средств: чеки активируются получателем, переводы зачисляются принудительно. Рассмотрим ключевые отличия.

Характеристика Чеки (Check) Переводы (Transfer)
Получатель Любой (через ссылку) Конкретный user_id
Действие получателя Должен активировать сам Пассивно (зачисляется автоматически)
Требование к получателю Любой Telegram-пользователь Должен запустить @CryptoBot
Настройка Включены по умолчанию Включаются в Security
Идемпотентность Нет (всегда создаётся новый) Есть (через spend_id)
Статусы active, activated completed
Макс. сумма ~25 000 USD ~25 000 USD
Уведомление Через бота при активации Можно отключить (disable_send_notification)
QR и Image check.qr, check.get_image() Нет визуальных элементов
Отмена delete_check() Нельзя отменить
Когда использовать Подарки, бонусы, промо Выплаты, зарплата, возвраты

💡 Что выбрать: Check или Transfer?

Чеки хороши для массовых раздач и промо-акций — получатель сам решает, когда активировать. Переводы — для гарантированных выплат конкретному пользователю. Если тебе нужно отправить средства и быть уверенным, что пользователь их получил — используй Transfer. Если хочешь создать «сюрприз» — используй Check.

Урок 5.1: Создание переводов

10 вопросов