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.

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-curlandext-jsonextensions (enabled by default on most hosts) - Composer
- an approved MercanPay merchant account and an API key with the
invoicesscope
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_idis idempotent. Sending the sameorder_idagain returns the existing invoice rather than a new one, so retrying after a timeout is safe.metadatais never shown to the customer. It comes back unchanged in webhooks and API responses.expires_in_minutessets 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.partialandinvoice.overpaidare 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.


