Урок 5.1 — Создание переводов
Научимся отправлять переводы пользователям Crypto Pay. Разберём параметры метода transfer(), идемпотентность через spend_id и типы данных Transfers.
Переводы (Transfers) — это возможность отправлять средства напрямую пользователям @CryptoBot через API. В отличие от чеков, где пользователь сам активирует получение, переводы отправляются принудительно: ты указываешь user_id получателя, и средства зачисляются ему на баланс. В этом уроке мы разберём, как настроить переводы, создать первый перевод и обеспечить идемпотентность.
Включение переводов в настройках приложения
Прежде чем использовать переводы, их нужно включить в настройках твоего приложения в @CryptoBot или @CryptoTestnetBot. По умолчанию переводы отключены для всех новых приложений.
🔧 Как включить переводы:
- Открой чат с @CryptoBot (или @CryptoTestnetBot для тестов)
- Перейди в раздел Crypto Pay
- Выбери своё приложение (или создай новое)
- Перейди в Security → Transfers
- Включи опцию Enable Transfers
- Подтверди изменения
‼️ Важно: переводы не включаются по умолчанию
Если ты попытаешься вызвать transfer() с выключенными переводами, API вернёт ошибку. Убедись, что переводы включены в настройках приложения перед использованием. Эта опция находится в разделе Security — она выключена по умолчанию для безопасности.
Метод transfer() — отправка перевода
Метод transfer() отправляет средства указанному пользователю @CryptoBot. Для получателя это выглядит как мгновенное зачисление на баланс в боте. Твоё приложение должно иметь достаточный баланс для отправки.
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 обязательны. Остальные параметры опциональны.
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
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 — уведомление отправляется). |
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 вернёт результат первого перевода.
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 или криптостойкие случайные строки
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 |
Дата создания перевода |
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- Средства всё равно зачислятся на баланс
- Полезно для массовых или фоновых выплат
# Перевод с комментарием — получатель увидит "Бонус!"
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 в эквиваленте для всех активов. Если нужно отправить больше — отправляй частями.
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 не сможет зачислить средства.
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 и повторить попытку, или использовать альтернативный способ отправки (например, чек).
Практические примеры
Рассмотрим несколько реальных сценариев отправки переводов.
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
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
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. Обрезай комментарий перед отправкой при необходимости.
Что важно запомнить
Практическое задание
Задание: Создай сервис отправки переводов
Напиши класс 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)) - Вести лог всех операций
Подсказка:
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.
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 вопросов