Приймання криптоплатежів на 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.

Інші гайди

Почніть приймати оплату вже сьогодні

Обліковий запис і перший проєкт за хвилину, без абонентської плати.

Створити обліковий запис