MercanPay Blog

WooCommerce Crypto Payments: Add USDT and TRX with PHP

Add WooCommerce crypto payments with a small custom gateway built on mercanpay-php: create invoices, verify webhooks, update order status and go live.

Published: November 8, 20267 min readTürkçe oku
WooCommerce Crypto Payments: Add USDT and TRX with PHP

WooCommerce crypto payments are a common request from WordPress store owners. The easiest route is our ready-made plugin: download the MercanPay WooCommerce plugin for free and install it from your WordPress dashboard in a few minutes. If you want full control over the flow or need to fit a custom store setup, a small custom gateway built on the official mercanpay/mercanpay-php library is straightforward too. This guide builds that gateway's skeleton step by step.

The WooCommerce crypto payments flow

It follows MercanPay's usual payment logic:

  1. At checkout the customer picks "Crypto (USDT / TRX)".
  2. WooCommerce creates the order and our gateway opens a MercanPay invoice.
  3. The customer is redirected to the MercanPay payment page, picks a coin and pays.
  4. Once the payment is final after 20 block confirmations, a signed webhook reaches your store.
  5. The webhook is verified and the order moves to "Processing".

We fulfil the order from the verified webhook, never from the browser redirect. That's the single most important rule of the integration.

Requirements

  • A WordPress site with WooCommerce and PHP 8 or newer
  • Composer on the server (or install locally and upload the plugin folder)
  • An approved MercanPay merchant account and an API key with the invoices scope
  • The webhook signing secret (whsec_…) from the Settings page of the panel

No account yet? Apply as a merchant first. The library basics are covered in the PHP crypto payment integration guide.

1. Prepare the plugin folder

Create wp-content/plugins/mercanpay-gateway/ and install the library inside it:

cd wp-content/plugins/mercanpay-gateway
composer require mercanpay/mercanpay-php

Keep the API key and signing secret out of the code. Defining constants in wp-config.php is a simple, safe option:

define('MERCANPAY_API_KEY', 'mp_...');
define('MERCANPAY_WEBHOOK_SECRET', 'whsec_...');

2. Write the gateway class

Every WooCommerce payment method extends WC_Payment_Gateway. This skeleton shows the essentials:

<?php
/*
 * Plugin Name: MercanPay Crypto Gateway (custom)
 */
require __DIR__ . '/vendor/autoload.php';

use MercanPay\MercanPay;
use MercanPay\MercanPayException;

add_action('plugins_loaded', function () {
    class WC_Gateway_MercanPay extends WC_Payment_Gateway
    {
        public function __construct()
        {
            $this->id                 = 'mercanpay';
            $this->method_title       = 'MercanPay (USDT / TRX)';
            $this->title              = 'Pay with crypto (USDT / TRX · TRC20)';
            $this->description        = 'Choose TRX or USDT on the payment page.';
            $this->has_fields         = false;
            $this->init_form_fields();
            $this->init_settings();
        }

        public function process_payment($order_id)
        {
            $order   = wc_get_order($order_id);
            $attempt = (int) $order->get_meta('_mercanpay_attempt') + 1;
            $zp      = new MercanPay('https://mercanpay.com', MERCANPAY_API_KEY);

            try {
                $invoice = $zp->createUsdInvoice(wc_format_decimal($order->get_total(), 2), [
                    'order_id'     => 'wc-' . $order->get_id() . '-' . $attempt,
                    'description'  => 'Order #' . $order->get_order_number(),
                    'success_url'  => $this->get_return_url($order),
                    'return_url'   => wc_get_checkout_url(),
                    'callback_url' => WC()->api_request_url('mercanpay'),
                    'metadata'     => ['wc_order' => $order->get_id()],
                ]);
            } catch (MercanPayException $e) {
                wc_add_notice('The payment page could not be opened. Please try again.', 'error');
                return ['result' => 'failure'];
            }

            $order->update_meta_data('_mercanpay_attempt', $attempt);
            $order->update_meta_data('_mercanpay_invoice', $invoice['id']);
            $order->update_status('pending', 'MercanPay invoice created.');
            $order->save();

            return ['result' => 'success', 'redirect' => $invoice['payment_url']];
        }
    }

    add_filter('woocommerce_payment_gateways', function ($methods) {
        $methods[] = 'WC_Gateway_MercanPay';
        return $methods;
    });
});

A few things to note:

  • Amounts are strings. wc_format_decimal produces a two-decimal string; don't pass floats.
  • Store currency. createUsdInvoice takes the price in USD. If your store prices in another currency, convert to USD with your own rate before creating the invoice.
  • Attempt number. A request with an existing order_id returns that invoice, even if it has expired. If the customer clicks "Pay" again after expiry, you need a fresh invoice, hence the attempt suffix (-1, -2). The reasoning is in idempotent invoices with order_id.
  • Minimum amount. The minimum invoice is $10. Override is_available() to hide the method for smaller carts.

What happens inside process_payment, step by step

The code is short, but every line has a job:

  1. The attempt counter goes up. The _mercanpay_attempt value stored on the order is incremented, so each new payment attempt for the same order gets its own invoice.
  2. The invoice is created. createUsdInvoice sends the cart total in USD. The customer picks TRX or USDT on the payment page, and the rate is locked at that moment.
  3. Redirect targets are set. success_url is WooCommerce's thank-you page, where the customer lands after paying. return_url is the target of the "Back to store" button on the payment page; here it takes the customer back to your checkout.
  4. The webhook address is attached. callback_url makes this invoice's events go to that address in addition to the Webhook URL set in the panel.
  5. The invoice id is saved on the order. _mercanpay_invoice lets the webhook handler check that an event belongs to the right invoice.
  6. The customer is redirected to the returned payment_url.

If invoice creation fails, the MercanPayException is caught and the customer sees a plain error message. Logging the details (getErrorCode() and getMessage()) makes problems much easier to diagnose later.

3. Add the webhook endpoint

The callback_url we passed, WC()->api_request_url('mercanpay'), fires WooCommerce's woocommerce_api_mercanpay hook. That's where we handle the webhook:

add_action('woocommerce_api_mercanpay', function () {
    $body   = file_get_contents('php://input');
    $header = $_SERVER['HTTP_X_MERCANPAY_SIGNATURE'] ?? '';

    if (!MercanPay::verifyWebhook(MERCANPAY_WEBHOOK_SECRET, $body, $header)) {
        status_header(401);
        exit;
    }

    $event   = json_decode($body, true);
    $invoice = $event['data']['invoice'] ?? [];
    $order   = wc_get_order((int) ($invoice['metadata']['wc_order'] ?? 0));

    if ($order && $order->get_meta('_mercanpay_invoice') === ($invoice['id'] ?? '')) {
        switch ($event['type']) {
            case 'invoice.paid':
            case 'invoice.overpaid':
                if (!$order->is_paid()) {
                    $order->payment_complete($invoice['id']);
                    $order->add_order_note('MercanPay: received ' . $invoice['paid_amount'] . ' ' . $invoice['token'] . '.');
                }
                break;
            case 'invoice.partial':
                $order->update_status('on-hold', 'MercanPay: underpaid, remaining ' . $invoice['remaining_amount']);
                break;
            case 'invoice.expired':
                $order->add_order_note('MercanPay: invoice expired.');
                break;
        }
    }

    status_header(200);
    exit;
});

Key details:

  • Verify the raw body. The signature covers the raw body before JSON decoding. See how to verify webhook signatures.
  • Match the invoice. We check that the event's invoice id is the latest one stored on the order, so an event from an old attempt can't affect the order.
  • Be idempotent. The is_paid() check means the order is processed once even if the event arrives twice.
  • Answer fast. Without a 2xx within 10 seconds the webhook is retried. Push slow work such as emails to the background.

A payment that arrives after the invoice expired is still credited to your balance, and data.payment.late is true in the invoice.paid event. How you handle such orders is your business rule; see handling underpayments, overpayments and late payments.

4. Test it

There's no separate test environment, so run a controlled live test:

  1. Use "Send test" on the Webhooks page of the panel to send a test.ping event and confirm in the delivery history that your endpoint returns 200. (That event has no order, so your code should simply skip it.)
  2. Place a real test order at the minimum ($10) and pay with USDT.
  3. Check that the order moves to "Processing" and has the order note.

Common mistakes

  • Confirming orders on the thank-you page: parameters appended to success_url can be forged. Only the webhook should change the order status.
  • Verifying decoded data instead of the raw body: the signature covers the raw body read from php://input. Decode and re-encode it first and the signature won't match.
  • Reusing the same order_id for every attempt: the expired invoice comes back and the customer can't pay. Keep the attempt suffix.
  • Doing slow work in the webhook: if stock syncs or emails push you past 10 seconds, the event is retried. Move that work to the background.
  • Caching and security plugins: some plugins block incoming POST requests from outside. Check the delivery history to confirm your webhook URL is reachable.

Go-live checklist

  • The API key and signing secret live in wp-config.php or environment variables, not in code.
  • The webhook URL uses HTTPS and signatures are verified.
  • Orders become paid only via the webhook, never via the thank-you page.
  • invoice.partial and invoice.overpaid are handled.
  • If your store currency isn't USD, the conversion is correct.
  • The flow was tested end to end with a small real payment.

FAQ

Is there an official WooCommerce plugin?

There isn't one right now. With the structure in this guide you can build your own gateway quickly.

Which coins will customers see?

TRX and USDT (TRC20) by default. To accept USDT only, add 'tokens' => ['USDT'] to the invoice options.

I use Laravel or another PHP framework.

The logic is the same. For Laravel, see Laravel crypto payment integration.

Can I issue refunds?

Crypto payments have no automatic refunds. A refund is a withdrawal from your balance to the address the customer gives you. See how to refund crypto payments.

Get started with MercanPay

A WooCommerce crypto payment method takes just a few files. Open your merchant account, grab an API key and check the API documentation for every field.

#WooCommerce#PHP#USDT#Integration

Related posts

Start accepting crypto payments in minutes

Accept USDT and TRX (TRC20). Fees from 0.4%, a hosted payment page, API and webhooks.