Приём криптоплатежей на Python

Минимальная интеграция Moonvex на Python с Flask и requests: создаём счёт, отправляем клиента на страницу оплаты, принимаем webhook и проверяем подпись HMAC-SHA256 стандартной библиотекой. Подходит для любого WSGI-приложения.

Пошагово

  1. 1.Получите ключ API и секрет webhook

    В кабинете Moonvex откройте «Интеграция» → «Создать ключ» → тип «Секретный» (sk_live_…). Ключ показывается один раз — сохраните его на сервере в MOONVEX_SECRET_KEY. Там же добавьте webhook на https-адрес вашего сервера и сохраните секрет подписи в MOONVEX_WEBHOOK_SECRET.

    pip install flask requests
    
    # переменные окружения на сервере
    export MOONVEX_SECRET_KEY=sk_live_...
    export MOONVEX_WEBHOOK_SECRET=whsec_...
  2. 2.Создайте платёж

    POST /v1/payments через requests: в заголовках Authorization: Bearer и Idempotency-Key, в теле orderId, amount строкой, currency и network. Ответ 201 — объект платежа с id и paymentUrl. Сохраните id у себя рядом с заказом.

    # app.py
    import os
    import sqlite3
    from contextlib import closing
    
    import requests
    from flask import Flask, abort, redirect
    
    app = Flask(__name__)
    API = "https://api.moonvex.org"
    
    
    def db() -> sqlite3.Connection:
        conn = sqlite3.connect("shop.db")
        conn.row_factory = sqlite3.Row
        return conn
    
    
    def create_payment(order_id: int, amount: str, attempt: int = 1) -> dict:
        r = requests.post(
            API + "/v1/payments",
            headers={
                "Authorization": "Bearer " + os.environ["MOONVEX_SECRET_KEY"],
                # повтор с тем же ключом не создаст второй счёт
                "Idempotency-Key": f"order-{order_id}-{attempt}",
            },
            json={"orderId": str(order_id), "amount": amount, "currency": "USDT", "network": "TRON"},
            timeout=15,
        )
        payment = r.json()
        if r.status_code >= 400:
            raise RuntimeError(payment["error"]["code"] + ": " + payment["error"]["message"])
        return payment
    
    
    @app.post("/orders/<int:order_id>/pay")
    def pay(order_id: int):
        with closing(db()) as conn:
            order = conn.execute("SELECT id, total FROM orders WHERE id = ?", (order_id,)).fetchone()
        if order is None:
            abort(404)
        # total хранится как TEXT "25.00" — сумма уходит строкой
        payment = create_payment(order["id"], str(order["total"]))
        return redirect(payment["paymentUrl"], code=303)
  3. 3.Отправьте клиента на paymentUrl

    paymentUrl — готовая страница оплаты: точная сумма с комиссией, адрес, QR-код, официальный контракт токена и таймер, статус обновляется сам. Открывайте её редиректом или в новой вкладке — встроить во фрейм нельзя. Возврат клиента на ваш сайт ещё не оплата: заказ отмечается оплаченным только по webhook или статусу из GET /v1/payments/:id. Во Flask — redirect(payment["paymentUrl"], code=303).

  4. 4.Примите webhook и проверьте подпись

    Moonvex шлёт POST с заголовком Moonvex-Signature: t=<unix-время>,v1=<подпись>. Подпись — HMAC-SHA256 секретом webhook от строки «t.сырое_тело» в hex. Считайте её по байтам тела до разбора JSON, сравнивайте за постоянное время, отклоняйте события старше 5 минут. После смены секрета 24 часа в заголовке два v1 — подходит любой. Во Flask сырое тело — request.get_data(), а hmac.compare_digest сравнивает подписи за постоянное время.

    # moonvex_signature.py
    import hashlib
    import hmac
    import time
    
    
    def verify_moonvex(raw: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
        """Проверяет Moonvex-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + "." + raw)>"""
        parts = [p.split("=", 1) for p in (header or "").split(",") if "=" in p]
        t = next((v for k, v in parts if k == "t"), None)
        if t is None or not t.isdigit() or abs(time.time() - int(t)) > tolerance:
            return False
        expected = hmac.new(secret.encode(), t.encode() + b"." + raw, hashlib.sha256).hexdigest()
        # compare_digest — сравнение за постоянное время; при смене секрета v1 два
        return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1")
    
    # app.py (продолжение)
    import json
    
    from flask import request
    
    from moonvex_signature import verify_moonvex
    
    
    @app.post("/moonvex/webhook")
    def moonvex_webhook():
        raw = request.get_data()  # сырые байты, до разбора JSON
        if not verify_moonvex(raw, request.headers.get("Moonvex-Signature", ""), os.environ["MOONVEX_WEBHOOK_SECRET"]):
            return "bad signature", 400
        event = json.loads(raw)
        if event["type"] in ("payment.confirmed", "payment.overpaid"):
            mark_paid_once(event["id"], event["data"]["orderId"])
        return "", 200
  5. 5.Отметьте заказ оплаченным ровно один раз

    При повторной доставке id события тот же, поэтому запишите его в таблицу с уникальным ключом в одной транзакции с обновлением заказа: дубль ничего не изменит. Выдавайте товар по payment.confirmed и payment.overpaid, не по payment.detected. В SQLite это INSERT OR IGNORE и проверка cursor.rowcount в одной транзакции.

    # app.py (продолжение)
    # CREATE TABLE processed_events (event_id TEXT PRIMARY KEY, created_at TEXT DEFAULT CURRENT_TIMESTAMP);
    
    
    def mark_paid_once(event_id: str, order_id: str) -> None:
        # второй "conn" — транзакция: commit при успехе, rollback при ошибке
        with closing(db()) as conn, conn:
            cur = conn.execute("INSERT OR IGNORE INTO processed_events (event_id) VALUES (?)", (event_id,))
            if cur.rowcount == 1:  # первая доставка события
                conn.execute("UPDATE orders SET status = 'paid' WHERE id = ? AND status <> 'paid'", (int(order_id),))
  6. 6.Проверьте интеграцию

    Тестовой сети у Moonvex нет — всё работает в основных сетях. Кнопка «Отправить тест» в разделе «Интеграция» пришлёт событие webhook.test — так проверяется подпись. Затем оплатите небольшой счёт, например 1 USDT в TRON: это обычный платёж, деньги придут на баланс. Журнал доставок с кодами ответов и кнопкой «Повторить» — там же.

Подводные камни

  • Используйте request.get_data(), а не request.json: подпись считается по исходным байтам, повторная сериализация их меняет.
  • Суммы держите в Decimal или строках. 0.1 + 0.2 во float — не 0.3, а в платежах это ошибка в центах.
  • Всегда задавайте timeout в requests.post — без него зависший запрос держит воркер бесконечно.
  • Функция verify_moonvex принимает все значения v1 из заголовка — так ротация секрета пройдёт без простоя.

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

Есть ли официальный Python SDK?

Официальный SDK есть для Node.js (@moonvex/node). Для Python достаточно requests или httpx: API — обычный HTTPS с JSON, а проверка подписи занимает десяток строк из этого гайда.

Что делать, если клиент не успел оплатить?

Платёж перейдёт в EXPIRED и придёт payment.expired. По тому же orderId можно создать новый платёж — с новым Idempotency-Key (например, увеличьте номер попытки в конце ключа). Срок оплаты задаётся в expiresInMinutes: от 5 до 1440 минут, по умолчанию 30.

Есть ли тестовый режим или sandbox?

Нет, тестовой сети нет — Moonvex работает только в основных сетях. Подпись проверяется кнопкой «Отправить тест» (событие webhook.test), а весь путь оплаты — реальным платежом на небольшую сумму, например 1 USDT в TRON.

Другие гайды

Начните принимать оплату сегодня

Аккаунт и первый проект за минуту, без абонентской платы.

Создать аккаунт