MK Pay Merchant API
Integrate USDT payments into your website in minutes. Create invoices from your server, let your customers pay on the MK Pay checkout page, and receive a signed webhook the moment the payment is confirmed.
How it works
The API is intentionally tiny: one endpoint to create an invoice and one webhook to receive confirmations. No SDKs, no complex handshakes — plain HTTPS, JSON and HMAC signatures.
PAID. The credited amount is what actually arrived after network fees.Key facts
| Item | Value |
|---|---|
| Currency | USDT only (BEP20 / BSC and Binance Pay C2C) |
| Confirmation time | Usually within a few minutes of the customer paying |
| Request authentication | HMAC-SHA512 headers (section 03) |
| Webhook authentication | X-GW-Signature — HMAC-SHA512 of the raw body (section 06) |
| Rate limit | 60 requests per minute per merchant |
02 · Getting Started
- Log in to your MK Pay merchant panel.
- Open Account & API — copy your
api_keyandapi_secret. - Open the Webhook page and save your site's webhook URL. It is used for any invoice that does not carry its own
notify_url. - Check Plugins & Modules first — ready-made modules exist for popular platforms. Use the raw API below only for custom integrations.
api_secret signs every request you send and every webhook you receive. Store it server-side only — never in browser JavaScript, mobile apps or public repositories. Requests can only be made while your merchant subscription is active; expired plans receive 403 subscription_required.03 · Authentication
Every request must carry four HTTP headers:
| Header | Value |
|---|---|
X-Merchant-Key | Your API key — identifies the merchant. |
X-Timestamp | Current Unix time in milliseconds (e.g. 1792764428123). |
X-Nonce | A random unique string per request — e.g. 16 hex characters. |
X-Signature | HMAC-SHA512 signature, computed as shown below. |
Signature recipe
string_to_sign = X-Timestamp + "\n" + X-Nonce + "\n" + raw_request_body
X-Signature = lowercase_hex( HMAC_SHA512( key = api_secret,
message = string_to_sign ) )Rules that prevent bad_signature
- Sign the exact bytes you send. Build the JSON string once, sign it, and POST that same string. Re-encoding or re-ordering fields afterwards breaks the signature.
- The timestamp must be within ±5 seconds of the MK Pay server clock. Generate it immediately before sending and keep your server clock synced via NTP.
- The nonce must be unique per request. Combined with the tight 5-second window, this blocks replay attempts.
- All calls are server-to-server over HTTPS — never call the API from a browser.
// $body = the exact JSON string you are about to POST $ts = (string) round(microtime(true) * 1000); // milliseconds $nc = bin2hex(random_bytes(8)); // random nonce $sig = hash_hmac('sha512', $ts . "\n" . $nc . "\n" . $body, $SECRET);
04 · Create Invoice
POST https://mkpay.win/api/v1/create.phpCreates a pending invoice and returns a checkout URL. Send the customer to redirect — everything else (amount display, method choice, payment detection) is handled by MK Pay.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | Unique customer identifier on your site. Allowed characters: A-Z a-z 0-9 _ - @ . — anything else is stripped. |
amount | number | Yes | Invoice amount in USDT, up to 2 decimals. Must be within the platform's allowed range, otherwise bad_amount. |
gateway_id | integer | No | Force a specific gateway (must be one of your active gateways). Omit to let MK Pay choose automatically. |
return_url | string (URL) | No | Where the customer is sent after completing the payment. |
notify_url | string (URL) | No | Per-invoice webhook override. If omitted, the webhook URL saved on your panel's Webhook page is used. |
reference | string | No | Your own order/reference ID. Allowed: A-Z a-z 0-9 _ -. Returned unchanged in the webhook. |
customer_name | string | No | Customer display name (max 100 chars). |
customer_email | string | No | Customer email (must be a valid email format). |
gateway_id is provided, that gateway is used. If omitted and you have exactly one payable method (a BEP20 gateway, or Binance Pay with a QR configured), it is assigned automatically. If you have more than one, the invoice is created unassigned and the customer chooses the method on the checkout page — the choice then locks the invoice.Minimal request
{
"user_id": "1007",
"amount": 10.00
}Success response — HTTP 200
{
"status": "OK",
"transaction_id": "GP2610231145027F3A9C",
"note": "483920",
"redirect": "https://mkpay.win/checkout.php?inv=9f3c8a41d2e74b06a5c1f8e3d2b4a6c8"
}| Field | Description |
|---|---|
status | OK — the invoice was created. |
transaction_id | Unique invoice ID (GP + timestamp + random). Store it — it comes back in the webhook as transaction_id. |
note | A 6-digit numeric note. Customers paying via Binance Pay must include it as the "Note to Payee"; it is the payment's identifier. |
redirect | The checkout page URL. Redirect the customer there (HTTP 302 or a link/button). |
0.0999 USDT to the amount so the on-chain transfer can be matched automatically. The customer always pays the exact amount displayed on the checkout page. Your integration must therefore never hardcode or assume amounts — always read the credited amount from the webhook (section 06).05 · Errors
All responses are JSON. Authentication and transport failures use proper HTTP status codes; two validation errors return HTTP 200 — so always check the error / status field in the body.
| Error | HTTP | Meaning |
|---|---|---|
missing_headers | 401 | One or more of the four authentication headers are missing. |
expired_timestamp | 401 | X-Timestamp differs from the MK Pay server clock by more than 5 seconds. |
unknown_merchant | 401 | The API key is invalid or the merchant account is disabled. |
bad_signature | 401 | The signature does not match — see the rules in section 03. |
subscription_required | 403 | Your merchant plan has expired — renew it in the panel. |
no_active_gateway | 422 | You have no active payment gateway. Add/activate one on the Gateways page. |
rate_limited | 429 | More than 60 requests in 60 seconds — slow down and retry. |
user_id_required | 200 * | user_id is missing or empty after sanitisation. |
bad_amount | 200 * | amount is outside the platform's allowed range. |
* Validation errors return HTTP 200 with an error field — inspect the response body, not just the status code.
\n separators · millisecond vs second precision in X-Timestamp.06 · Webhooks
The moment an invoice is confirmed, MK Pay POSTs a signed JSON payload to your webhook URL. This is how you know — with cryptographic certainty — that the payment really happened and was really confirmed by MK Pay.
Which URL receives the webhook?
- The
notify_urlsent when creating that specific invoice (per-invoice override) — used first if present. - Otherwise, the webhook URL saved on your panel's Webhook page.
Delivery facts
| Item | Value |
|---|---|
| Method | POST, Content-Type: application/json |
| Signature header | X-GW-Signature: lowercase_hex(HMAC_SHA512(raw_body, api_secret)) |
| Timeout | 10 seconds — reply fast, defer heavy work |
| Attempts | One attempt per confirmation — keep your endpoint reachable, and contact support to re-send if you missed one |
| Expected answer | HTTP 200 with the body OK |
Payload fields
| Field | Type | Description |
|---|---|---|
transaction_id | string | The invoice ID — the same one returned by Create Invoice. Use it for idempotency. |
note | string | The 6-digit payment note. |
reference | string | Your reference, exactly as sent ("" if none). |
customer | string | The user_id you sent — credit this user. |
customer_name | string | Customer name, as sent. |
customer_email | string | Customer email, as sent. |
amount | number | The credited amount — what actually arrived after network fees. Credit this value. |
invoice_amount | number | The original invoice amount (informational only). |
currency | string | USDT |
status | string | PAID — only process payloads with this status. |
ts | number | Unix timestamp of the notification. |
amount (credited) can be slightly lower than invoice_amount. Always credit amount. If your platform validates amount == invoice total, it will reject perfectly valid payments.Verifying the signature (PHP)
// Compare in constant time; reject anything that does not match
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_GW_SIGNATURE'] ?? '';
if (!hash_equals(hash_hmac('sha512', $raw, $SECRET), $sig)) {
http_response_code(403);
echo 'invalid signature';
exit;
}X-GW-Signature check above, an attacker credits themselves for free. Verify first, process second, answer 200 OK last.07 · Security Best Practices
- Server-to-server only. The
api_secretmust never appear in browser JavaScript, mobile apps, or public repos — anyone holding it can create invoices and forge nothing less than your identity. - Verify every webhook signature with a constant-time comparison (
hash_equalsin PHP) before doing anything else. - Idempotency: keep a unique index on
transaction_idand skip payloads you already processed — duplicates then become harmless. - Trust only
status: "PAID". Ignore or log anything else. - Map before crediting: use
reference/customerto tie the payment to a real order on your side before adding balance. - Use HTTPS everywhere — for
return_url,notify_url, and your webhook endpoint. - Log every webhook (raw body + your processing outcome). When something looks wrong, that log is the single source of truth.
08 · Full Integration Examples
Two small PHP files are all it takes. Adapt the TODO sections to your platform.
1 — Creating an invoice from your server
<?php /* MK Pay — create an invoice (call this from your SERVER, never the browser) */ $API_URL = 'https://mkpay.win/api/v1/create.php'; $API_KEY = 'YOUR_API_KEY'; $SECRET = 'YOUR_API_SECRET'; $payload = [ 'user_id' => '1007', // customer ID on YOUR site 'amount' => 10.00, // USDT, max 2 decimals 'reference' => 'ORDER-8891', // optional: your order id 'return_url' => 'https://mysite.com/thanks.php', 'notify_url' => 'https://mysite.com/mkpay-webhook.php', 'customer_name' => 'Mohamed Ahmed', 'customer_email' => '[email protected]', ]; $body = json_encode($payload); // sign EXACTLY these bytes $ts = (string) round(microtime(true) * 1000); // milliseconds $nc = bin2hex(random_bytes(8)); // random nonce $sig = hash_hmac('sha512', $ts . "\n" . $nc . "\n" . $body, $SECRET); $ch = curl_init($API_URL); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Merchant-Key: ' . $API_KEY, 'X-Timestamp: ' . $ts, 'X-Nonce: ' . $nc, 'X-Signature: ' . $sig, ], ]); $res = json_decode((string) curl_exec($ch), true); if (($res['status'] ?? '') === 'OK') { header('Location: ' . $res['redirect']); // send customer to checkout exit; } http_response_code(400); exit('MK Pay error: ' . ($res['error'] ?? 'unknown'));
2 — Receiving the payment webhook
<?php /* MK Pay — webhook receiver. Verify first, process second, answer last. */ $SECRET = 'YOUR_API_SECRET'; $raw = file_get_contents('php://input'); $sig = $_SERVER['HTTP_X_GW_SIGNATURE'] ?? ''; /* 1) Verify authenticity (constant-time compare) */ if (!hash_equals(hash_hmac('sha512', $raw, $SECRET), $sig)) { http_response_code(403); echo 'invalid signature'; exit; } /* 2) Parse the payload */ $d = json_decode($raw, true); /* 3) Process only confirmed payments, exactly once (idempotency) */ if (($d['status'] ?? '') === 'PAID') { $txn = $d['transaction_id']; // e.g. GP2610231145027F3A9C — unique key $user = $d['customer']; // the user_id you sent $amt = (float) $d['amount']; // CREDIT THIS (arrived amount) // TODO: skip if $txn was already processed // TODO: credit $amt to user $user on your platform } /* 4) Always answer 2xx so MK Pay marks the delivery as accepted */ http_response_code(200); echo 'OK';
mkpay-webhook.php to your site, then either send notify_url with every invoice, or save https://yoursite.com/mkpay-webhook.php once on the Webhook page of your MK Pay panel — every future invoice will use it automatically.09 · FAQ
Which amount should I credit my customer?
Always amount — the amount that actually arrived after USDT network fees. invoice_amount is informational only. This is MK Pay's arrived-amount policy and it protects both sides from fee surprises.
The checkout amount looks slightly different from what I requested — why?
For BEP20 invoices MK Pay adds a unique fraction of up to 0.0999 USDT so the on-chain transfer can be matched automatically. The customer pays the exact amount shown on the checkout page, and the webhook tells you the final credited value.
How long until the webhook arrives?
Usually within a few minutes of the customer completing the transfer. BEP20 payments wait for the required network confirmations; Binance Pay transfers are detected via the 6-digit note. Detection runs continuously — no manual step.
My site was down and missed the webhook — what now?
Check the invoice status on the Transactions page of your MK Pay panel, then contact support with the transaction_id to have the notification re-sent. To stay safe in the future, design your receiver to answer 200 OK within 10 seconds and defer heavy work.
Why do I keep getting expired_timestamp?
Your server clock drifts more than 5 seconds from MK Pay's clock. Enable NTP time synchronisation on your server and generate the timestamp immediately before each request.
Can I call the API from the browser or a mobile app?
No. The api_secret would be exposed and anyone could create invoices in your name. Always call the API from your server; the browser only ever sees the redirect URL.
Does MK Pay retry failed webhook deliveries?
Currently one attempt is made per confirmation. Keep your endpoint online and fast, monitor your logs, and contact support with the transaction ID if a delivery was missed.
Need help with your integration? Contact MK Pay support through the channels provided with your merchant account — and always include the transaction_id.