Криптоплатежи в WordPress и WooCommerce через API

Готового плагина Moonvex для WordPress и WooCommerce пока нет — подключение делается своим кодом через API. Ниже — небольшой собственный плагин: способ оплаты WooCommerce, который создаёт платёж и ведёт клиента на страницу оплаты, и REST-маршрут для webhook с проверкой подписи.

Пошагово

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

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

    // wp-config.php — ключи вне базы данных и вне репозитория темы
    define('MOONVEX_SECRET_KEY', 'sk_live_...');
    define('MOONVEX_WEBHOOK_SECRET', 'whsec_...');
  2. 2.Добавьте способ оплаты WooCommerce

    Класс WC_Gateway_Moonvex наследует WC_Payment_Gateway. В process_payment он создаёт платёж через wp_remote_post и возвращает redirect на paymentUrl. Цена в USD передаётся как currency: USD с монетой оплаты asset: USDT. Номер попытки хранится в мета-поле заказа и растёт только после успешного создания.

    <?php
    /**
     * Plugin Name: Moonvex Crypto Payments (custom)
     * Description: Своя интеграция WooCommerce с API Moonvex
     */
    // wp-content/plugins/moonvex-custom/moonvex-custom.php
    
    add_action('plugins_loaded', function () {
        if (!class_exists('WC_Payment_Gateway')) {
            return;
        }
    
        class WC_Gateway_Moonvex extends WC_Payment_Gateway
        {
            public function __construct()
            {
                $this->id = 'moonvex';
                $this->method_title = 'Moonvex';
                $this->method_description = 'Оплата в USDT через страницу оплаты Moonvex';
                $this->has_fields = false;
                $this->init_form_fields();
                $this->init_settings();
                $this->title = $this->get_option('title', 'Криптовалюта (USDT)');
                add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
            }
    
            public function process_payment($order_id)
            {
                $order = wc_get_order($order_id);
                if ($order->get_currency() !== 'USD') {
                    wc_add_notice('Оплата криптовалютой доступна для цен в USD', 'error');
                    return ['result' => 'failure'];
                }
    
                // номер попытки растёт только после успешного создания — повтор после таймаута идемпотентен
                $attempt = (int) $order->get_meta('_moonvex_attempts') + 1;
                $res = wp_remote_post('https://api.moonvex.org/v1/payments', [
                    'timeout' => 15,
                    'headers' => [
                        'Authorization' => 'Bearer ' . MOONVEX_SECRET_KEY,
                        'Idempotency-Key' => 'wc-order-' . $order->get_id() . '-' . $attempt,
                        'Content-Type' => 'application/json',
                    ],
                    'body' => wp_json_encode([
                        'orderId' => (string) $order->get_id(),
                        'amount' => wc_format_decimal($order->get_total(), 2),
                        'currency' => 'USD',
                        'asset' => 'USDT',
                        'network' => 'TRON',
                        'customerEmail' => $order->get_billing_email(),
                    ]),
                ]);
    
                $payment = is_wp_error($res) ? null : json_decode(wp_remote_retrieve_body($res), true);
                if (!$payment || empty($payment['paymentUrl'])) {
                    wc_add_notice('Не удалось создать платёж, попробуйте ещё раз', 'error');
                    return ['result' => 'failure'];
                }
    
                $order->update_meta_data('_moonvex_attempts', $attempt);
                $order->update_meta_data('_moonvex_payment_id', $payment['id']);
                $order->update_status('pending', 'Moonvex: ' . $payment['id']);
                $order->save();
    
                return ['result' => 'success', 'redirect' => $payment['paymentUrl']];
            }
        }
    
        add_filter('woocommerce_payment_gateways', function ($gateways) {
            $gateways[] = 'WC_Gateway_Moonvex';
            return $gateways;
        });
    });
  3. 3.Отправьте клиента на paymentUrl

    paymentUrl — готовая страница оплаты: точная сумма с комиссией, адрес, QR-код, официальный контракт токена и таймер, статус обновляется сам. Открывайте её редиректом или в новой вкладке — встроить во фрейм нельзя. Возврат клиента на ваш сайт ещё не оплата: заказ отмечается оплаченным только по webhook или статусу из GET /v1/payments/:id. WooCommerce сам перенаправит покупателя по redirect из process_payment.

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

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

    // moonvex-custom.php (продолжение)
    // URL для кабинета Moonvex: https://shop.example.com/wp-json/moonvex/v1/webhook
    add_action('rest_api_init', function () {
        register_rest_route('moonvex/v1', '/webhook', [
            'methods' => 'POST',
            'permission_callback' => '__return_true', // доступ проверяется подписью
            'callback' => function (WP_REST_Request $request) {
                $raw = $request->get_body(); // сырое тело
                if (!moonvex_verify($raw, (string) $request->get_header('Moonvex-Signature'), MOONVEX_WEBHOOK_SECRET)) {
                    return new WP_REST_Response('bad signature', 400);
                }
                moonvex_handle_event(json_decode($raw, true));
                return new WP_REST_Response('ok', 200);
            },
        ]);
    });
    
    function moonvex_verify(string $raw, string $header, string $secret, int $tolerance = 300): bool
    {
        $t = null;
        $sigs = [];
        foreach (explode(',', $header) as $part) {
            [$k, $v] = array_pad(explode('=', $part, 2), 2, '');
            if ($k === 't' && ctype_digit($v)) { $t = (int) $v; }
            if ($k === 'v1') { $sigs[] = $v; }
        }
        if (!$t || abs(time() - $t) > $tolerance) {
            return false;
        }
        $expected = hash_hmac('sha256', $t . '.' . $raw, $secret);
        foreach ($sigs as $s) {
            if (hash_equals($expected, $s)) {
                return true;
            }
        }
        return false;
    }
  5. 5.Отметьте заказ оплаченным ровно один раз

    При повторной доставке id события тот же, поэтому запишите его в таблицу с уникальным ключом в одной транзакции с обновлением заказа: дубль ничего не изменит. Выдавайте товар по payment.confirmed и payment.overpaid, не по payment.detected. В примере id события сохраняется в мета-поле заказа, а payment_complete() вызывается только если !$order->is_paid(). По payment.expired заказ уходит в «Не удался».

    // moonvex-custom.php (продолжение)
    function moonvex_handle_event(array $event): void
    {
        $order = wc_get_order((int) ($event['data']['orderId'] ?? 0));
        if (!$order || $order->get_payment_method() !== 'moonvex') {
            return;
        }
        $seen = '_moonvex_event_' . $event['id'];
        if ($order->get_meta($seen)) {
            return; // это событие уже обработано
        }
    
        switch ($event['type']) {
            case 'payment.confirmed':
            case 'payment.overpaid':
                if (!$order->is_paid()) {
                    $order->payment_complete($event['data']['id']); // pay_… — номер транзакции заказа
                }
                break;
            case 'payment.expired':
                if (!$order->is_paid()) {
                    $order->update_status('failed', 'Moonvex: payment expired');
                }
                break;
        }
    
        $order->update_meta_data($seen, time());
        $order->save();
    }
  6. 6.Проверьте интеграцию

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

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

  • Готового плагина Moonvex для WooCommerce пока нет — код из гайда ваш, обновляйте его вместе с магазином.
  • Блочное оформление заказа (Checkout block) показывает только шлюзы с интеграцией для блоков. Для кода из гайда используйте классический шорткод [woocommerce_checkout] или добавьте регистрацию через AbstractPaymentMethodType.
  • Валюта магазина должна быть USD (или монета): Moonvex принимает цену в USD или в криптовалюте, другие фиатные валюты нужно пересчитать самим.
  • Плагины безопасности и кэширования иногда закрывают /wp-json/ — откройте маршрут moonvex/v1/webhook для POST.

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

Есть ли готовый плагин Moonvex для WordPress?

Пока нет. Подключение — через API, как в этом гайде: около ста строк PHP в собственном плагине. Если не хотите писать код, счета можно выставлять вручную из кабинета Moonvex.

Покупатель вернулся на сайт, а заказ не оплачен — почему?

Возврат на сайт не подтверждает оплату. Заказ станет оплаченным, когда придёт webhook payment.confirmed — после нужного числа подтверждений сети. Проверьте журнал доставок в разделе «Интеграция».

Почему amount в ответе больше цены заказа?

Комиссию Moonvex платит клиент сверху, поэтому amount — итог к оплате. Ваша цена остаётся в pricing.amount, ваш доход при полной оплате — в merchantAmount. Сверяйте заказ по orderId и pricing.amount, а клиенту показывайте amount.

Другие гайды

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

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

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