MercanPay Blog

PHP Crypto Payment Integration: Accept USDT and TRX

A PHP crypto payment integration with the mercanpay-php library: create invoices, redirect to the payment page and verify signed webhooks, with full code.

Published: September 30, 20265 min readTürkçe oku
PHP Crypto Payment Integration: Accept USDT and TRX

A PHP crypto payment integration takes only a few files with the official mercanpay/mercanpay-php library. You create an invoice at checkout, redirect the customer to the hosted payment page, and fulfil the order when a signed webhook arrives. This guide wires USDT and TRX (TRC20) payments into a PHP site step by step.

Requirements

  • PHP 8 or newer
  • the ext-curl and ext-json extensions (enabled by default on most hosts)
  • Composer
  • an approved MercanPay merchant account and an API key with the invoices scope

If you don't have an account yet, apply as a merchant first. You create API keys on the API Keys page of the panel.

1. Install the library

composer require mercanpay/mercanpay-php

The library has no dependencies besides curl. Keep your API key and webhook signing secret in environment variables rather than in code:

MERCANPAY_API_KEY=mp_...
MERCANPAY_WEBHOOK_SECRET=whsec_...

Never put an API key in browser code, a mobile app or a public Git repository. If you suspect a leak, revoke the key in the panel right away.

2. Create an invoice for the crypto payment

When the customer clicks "Pay", your server creates an invoice and redirects to the payment page. There are two options.

USD-priced invoice (customer picks the coin)

<?php
require 'vendor/autoload.php';

use MercanPay\MercanPay;
use MercanPay\MercanPayException;

$zp = new MercanPay('https://mercanpay.com', getenv('MERCANPAY_API_KEY'));

try {
    $invoice = $zp->createUsdInvoice('49.90', [
        'order_id'     => 'order-1001',
        'description'  => 'Pro licence (1 year)',
        'success_url'  => 'https://myshop.com/order/1001/thanks',
        'return_url'   => 'https://myshop.com/cart',
        'callback_url' => 'https://myshop.com/mercanpay-webhook.php',
        'metadata'     => ['customer_id' => 42],
    ]);
    header('Location: ' . $invoice['payment_url']);
    exit;
} catch (MercanPayException $e) {
    error_log('MercanPay: ' . $e->getErrorCode() . ' ' . $e->getMessage());
    http_response_code(502);
    echo 'The payment page could not be opened. Please try again.';
}

The customer chooses TRX or USDT on the payment page and the rate is locked at that point. To limit the choice, pass 'tokens' => ['USDT'].

Fixed-coin invoice

$invoice = $zp->createInvoice('USDT', '49.90', ['order_id' => 'order-1001']);

A few details that matter:

  • Amounts are always strings, such as '49.90'. Don't use floats, because rounding errors can break a payment.
  • order_id is idempotent. Sending the same order_id again returns the existing invoice rather than a new one, so retrying after a timeout is safe.
  • metadata is never shown to the customer. It comes back unchanged in webhooks and API responses.
  • expires_in_minutes sets the invoice lifetime between 5 and 1440 minutes (24 hours).
  • The minimum invoice is $10. USDT invoices include a 1 USDT network fee paid by the customer.

Every field is listed in the API documentation.

3. Write the webhook endpoint

Once a payment is final after 20 block confirmations (usually about a minute), MercanPay sends a signed POST to your server. Enter your endpoint under Settings → Webhook URL in the panel. You can also pass a per-invoice callback_url, as above.

mercanpay-webhook.php:

<?php
require 'vendor/autoload.php';

use MercanPay\MercanPay;

$body   = file_get_contents('php://input');           // raw body, before json_decode
$header = $_SERVER['HTTP_X_MERCANPAY_SIGNATURE'] ?? '';

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

$event   = json_decode($body, true);
$invoice = $event['data']['invoice'] ?? null;

switch ($event['type']) {
    case 'invoice.paid':
    case 'invoice.overpaid':
        markOrderPaid($event['id'], $invoice['order_id'], $invoice['token'], $invoice['paid_amount']);
        break;
    case 'invoice.partial':
        // underpaid: remind the customer of the remaining amount
        break;
    case 'invoice.expired':
    case 'invoice.cancelled':
        // cancel the order or offer a new invoice
        break;
}

http_response_code(200);

verifyWebhook checks three things: the header has the form t=...,v1=..., the timestamp is no older than 5 minutes, and the HMAC-SHA256 signature matches under a constant-time comparison. The theory behind it is in How to verify webhook signatures.

4. Fulfil orders idempotently

Webhooks can be retried, so the same event may reach you more than once. Record the event id ($event['id']) so an order is never fulfilled twice:

function markOrderPaid(string $eventId, string $orderId, string $token, string $paid): void
{
    $pdo = new PDO(getenv('DB_DSN'), getenv('DB_USER'), getenv('DB_PASS'));
    $pdo->beginTransaction();

    // processed_events.event_id must be UNIQUE
    $stmt = $pdo->prepare('INSERT IGNORE INTO processed_events (event_id) VALUES (?)');
    $stmt->execute([$eventId]);
    if ($stmt->rowCount() === 0) {     // already handled
        $pdo->rollBack();
        return;
    }

    $pdo->prepare("UPDATE orders SET status = 'paid', paid_token = ?, paid_amount = ? WHERE id = ? AND status <> 'paid'")
        ->execute([$token, $paid, $orderId]);
    $pdo->commit();
}

Also check that the coin and amount in the webhook match your order.

Respond quickly, too. If you don't return a 2xx within 10 seconds, the request is retried with increasing delays, so push slow work such as sending emails onto a queue.

5. A success page is not a payment confirmation

After paying, the customer's browser is redirected to success_url with invoice_id, order_id and status appended. It's fine to show "Thanks for your payment" there, but don't fulfil the order from that redirect, because URL parameters can be forged. When in doubt, query the API:

$invoice = $zp->getInvoiceByOrder($_GET['order_id'] ?? '');
if (in_array($invoice['status'], ['paid', 'overpaid'], true)) {
    echo 'Your payment is confirmed. Thank you!';
} else {
    echo 'Your payment is being confirmed. You will get an email shortly.';
}

6. Error handling

Failed requests throw a MercanPayException:

Method Returns
getHttpStatus() The HTTP status (e.g. 400, 401, 403)
getErrorCode() A machine-readable code (e.g. validation_error, unauthorized)
getMessage() A human-readable description

On 429 (rate limit) and 5xx errors, wait briefly and retry the same request. Thanks to order_id, you'll never get a duplicate invoice.

Using WooCommerce or another platform?

There is currently no ready-made WooCommerce or WHMCS plugin for MercanPay. Both platforms are written in PHP, though, so you can build your own payment module from the steps above. The logic stays the same: call createUsdInvoice at checkout and update the order status in the webhook.

Go-live checklist

  • The API key lives only on the server, in an environment variable.
  • The webhook URL uses HTTPS and signatures are verified.
  • Every order has a unique order_id.
  • invoice.partial and invoice.overpaid are handled.
  • Orders are fulfilled from the webhook, not the browser redirect.
  • The whole flow has been tested end to end with a small real payment.

FAQ

How do I test the webhook?

The "Send test" button on the Webhooks page of the panel sends a test.ping event. The delivery history shows the response code for each request and lets you resend it.

Is PHP 7 supported?

The library targets PHP 8 and newer. On older versions, you can call the REST API directly with curl.

Can I send payouts from PHP?

Yes. With a key that has the withdrawals scope, call createWithdrawal('USDT', '50', 'T...', 'unique-key'). For security, API withdrawals can only go to saved addresses marked as trusted in the panel.

Get started with MercanPay

Everything you need for a PHP crypto payment integration is in one Composer package. Working in Python instead? See the Python integration guide. Open your merchant account, grab an API key and create your first invoice today.

#PHP#API#Integration#USDT

Related posts

Start accepting crypto payments in minutes

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