UPG API Integration Guide
UPG (USDT Pay Gateway) is a non-custodial on-chain USDT payment gateway. Merchants create payment orders via the API; funds are sent directly to the merchant's configured wallet address — the platform never holds funds.
API Basics
| Base URL | https://your-domain.com |
| Request Format | JSON body (Content-Type: application/json) |
| Response Format | JSON envelope: {"code":200,"message":"ok","data":{...}} |
| Character Encoding | UTF-8 |
| Supported Chains | TRC20 (TRON) / BEP20 (BSC) / ERC20 (ETH) |
Quick Start
The example below shows how to create your first USDT payment order and handle the callback in about 5 minutes.
// 1. Build signature
$params = [
'api_key' => 'YOUR_API_KEY',
'merchant_order_no' => 'ORDER_' . time(),
'amount' => 100.00,
'currency' => 'USDT',
'chain' => 'TRC20',
'notify_url' => 'https://your-site.com/notify',
'timestamp' => time(),
'nonce' => bin2hex(random_bytes(8)),
];
ksort($params);
$parts = [];
foreach ($params as $k => $v) {
if ($v === '' || $v === null) continue;
$parts[] = $k . '=' . $v;
}
$signStr = implode('&', $parts); // api_secret is the HMAC key only, not appended to the string
$params['sign'] = hash_hmac('sha256', $signStr, 'YOUR_API_SECRET');
// 2. Send request
$ch = curl_init('https://your-domain.com/api/pay/create');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$resp = json_decode(curl_exec($ch), true);
// 3. Handle response
if ($resp['code'] === 200) {
$order = $resp['data'];
// Redirect user to checkout page
header('Location: ' . $order['pay_url']);
}
import hmac, hashlib, time, random, string, requests
api_key = 'YOUR_API_KEY'
api_secret = 'YOUR_API_SECRET'
params = {
'api_key': api_key,
'merchant_order_no': f'ORDER_{int(time.time())}',
'amount': 100.00,
'currency': 'USDT',
'chain': 'TRC20',
'notify_url': 'https://your-site.com/notify',
'timestamp': int(time.time()),
'nonce': ''.join(random.choices(string.ascii_lowercase, k=16)),
}
sorted_params = dict(sorted(params.items()))
sign_str = '&'.join([f'{k}={v}' for k, v in sorted_params.items() if v != '' and v is not None])
# api_secret is the HMAC key only, not appended to the string
params['sign'] = hmac.new(api_secret.encode(), sign_str.encode(), hashlib.sha256).hexdigest()
resp = requests.post('https://your-domain.com/api/pay/create', json=params, timeout=10)
data = resp.json()
if data['code'] == 200:
print('Checkout:', data['data']['pay_url'])
const crypto = require('crypto');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
const API_SECRET = 'YOUR_API_SECRET';
const params = {
api_key: API_KEY,
merchant_order_no:`ORDER_${Date.now()}`,
amount: 100.00,
currency: 'USDT',
chain: 'TRC20',
notify_url: 'https://your-site.com/notify',
timestamp: Math.floor(Date.now() / 1000),
nonce: crypto.randomBytes(8).toString('hex'),
};
// api_secret is the HMAC key only, not appended to the string
const signStr = Object.keys(params).sort()
.filter(k => params[k] !== '' && params[k] != null)
.map(k => `${k}=${params[k]}`).join('&');
params.sign = crypto.createHmac('sha256', API_SECRET).update(signStr).digest('hex');
const { data } = await axios.post('https://your-domain.com/api/pay/create', params);
if (data.code === 200) console.log('Checkout:', data.data.pay_url);
Authentication
All business endpoints (create order, query order, etc.) require HMAC-SHA256 signature authentication. Each request must include the following common parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| api_key | string | Required | Merchant API Key, obtained from API Settings in the console |
| timestamp | int | Required | Current Unix timestamp (seconds); server allows ±300 seconds drift |
| nonce | string | Required | Random string, ≥8 characters; must not be reused within 15 minutes (replay protection) |
| sign | string | Required | HMAC-SHA256 signature, lowercase hex; see next section for algorithm |
merchant_order_no or tx_hash for idempotency.
Signature Algorithm
ksort($params) or Object.keys(params).sort()key=value pairs with &, skipping empty values. Do not append api_secret to the string — api_secret is used only as the HMAC key in the next step.sign.Signature Example
Parameters: api_key=abc, amount=100, nonce=xyz123, timestamp=1700000000
# Sorted and joined (lexicographic order):
api_key=abc&amount=100&nonce=xyz123×tamp=1700000000
# This is the complete signature string; api_secret is not in the string, only used as HMAC key
# PHP:
$sign = hash_hmac('sha256', $signStr, 'YOUR_SECRET');
# Python:
sign = hmac.new(secret.encode(), sign_str.encode(), hashlib.sha256).hexdigest()
# Node.js:
sign = crypto.createHmac('sha256', secret).update(signStr).digest('hex');
Create Payment Order
Create a USDT payment order. If an unpaid, non-expired order with the same merchant_order_no already exists, the original order is returned (idempotent).
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| merchant_order_no | string | Required | Unique merchant order ID, ≤64 characters |
| amount | number | Required | Payment amount, >0 |
| currency | string | Optional | Fiat currency: CNY (default) / USD / USDT |
| chain | string | Optional | Payment chain: TRC20 / BEP20 / ERC20. Official shop plugins and the general SDK example omit this; chain selection is handled on the UPG checkout page. Advanced integrations may specify a chain. |
| notify_url | string | Optional | Async callback URL; overrides default configured in console |
| return_url | string | Optional | Redirect URL after successful payment on checkout page |
| lang | string | Optional | Checkout language: en (default, /pay.html) / zh (/zh-cn/pay.html); alias locale; included in signature |
| attach | string | Optional | Pass-through parameter, returned as-is in callback, ≤500 characters |
| api_key / timestamp / nonce / sign | — | Required | Common auth parameters; see Authentication |
Response data
| Field | Type | Description |
|---|---|---|
| order_no | string | System order ID, format UPG + timestamp + random |
| merchant_order_no | string | Merchant order ID (echoed back) |
| chain | string|null | Payment chain |
| chain_pending | bool | Whether chain selection is pending (currently always false for create) |
| pay_address | string|null | USDT receiving address |
| usdt_amount | string | Exact USDT amount to transfer |
| fiat_amount | number | Fiat amount |
| fiat_currency | string | Fiat currency code |
| exchange_rate | float | Exchange rate snapshot at order time (1 USDT = X fiat) |
| pay_url | string | Checkout page URL (lang=en → /pay.html, lang=zh → /zh-cn/pay.html); redirect users here to pay |
| pay_locale | string | Checkout language: en or zh |
| return_url | string | Redirect URL after successful payment |
| qr_url | string | QR code data URL |
| expire_at | string | Order expiry time Y-m-d H:i:s |
| expire_seconds | int | Seconds until expiry |
| status | int | 0 = Pending payment |
| status_text | string | Human-readable status description |
Response Example
{
"code": 200,
"message": "Order created successfully",
"data": {
"order_no": "UPG20240101120000A1B2C3",
"merchant_order_no": "ORDER_1704067200",
"chain": "TRC20",
"pay_address": "TNVxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"usdt_amount": "100.000000",
"fiat_amount": 100,
"fiat_currency": "USDT",
"exchange_rate": 1,
"pay_url": "https://your-domain.com/pay.html?order=UPG20240101120000A1B2C3",
"pay_locale": "en",
"qr_url": "https://your-domain.com/pay/qr?order=UPG...",
"expire_at": "2024-01-01 12:30:00",
"expire_seconds": 1800,
"status": 0
}
}
Query Order Status
Query the current status of an order by system order ID or merchant order ID.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| order_no | string | One of | System order ID |
| merchant_order_no | string | One of | Merchant order ID |
| api_key / timestamp / nonce / sign | — | Required | Common auth parameters |
Response data
| Field | Type | Description |
|---|---|---|
| order_no | string | System order ID |
| merchant_order_no | string | Merchant order ID |
| status | int | See Order Status Codes |
| status_text | string | Human-readable status description |
| chain | string | Payment chain |
| pay_address | string | Receiving address |
| usdt_amount | string | Expected USDT amount |
| actual_amount | string | Actual USDT paid (filled after on-chain confirmation) |
| fiat_amount | number | Fiat amount |
| fiat_currency | string | Fiat currency code |
| exchange_rate | float | Exchange rate snapshot at order time |
| tx_hash | string | On-chain transaction hash |
| from_address | string | Payer address |
| confirmations | int | Block confirmations |
| paid_at | string|null | Payment time |
| expire_at | string | Expiry time |
| created_at | string | Creation time |
Query Available Chains
Query payment chains configured for the current merchant. Optionally pass an amount for billing pre-check. Recommended before creating orders to avoid requests that will fail.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | number | Optional | Used with currency for billing pre-check |
| currency | string | Optional | CNY / USD / USDT, default USDT |
| api_key / timestamp / nonce / sign | — | Required | Common auth parameters |
Response data
| Field | Type | Description |
|---|---|---|
| chains | array | Configured chains with addresses, e.g. ["TRC20","BEP20"] |
| chain_options | array | Per-chain details: chain, enabled, reason, notice |
| billing_active | bool | Whether billing status is healthy |
| billing_notice | string | Notice when billing is abnormal |
Callback Specification
OKOK within 10 seconds, or the system will retry with exponential backoff, up to 5 times.
When Callbacks Are Sent
| Scenario | Behavior |
|---|---|
| On-chain payment matched to order | Queued immediately, async POST to notify_url |
| Manually marked paid in console | No automatic callback; use Resend in Payment Records |
| Authorization expired / insufficient credits | Callback skipped; renew and resend manually from console |
Callback Parameters
| Parameter | Type | Description |
|---|---|---|
| order_no | string | System order ID |
| merchant_order_no | string | Merchant order ID |
| status | int | 1 = Paid (callbacks are only sent on successful payment) |
| chain | string | Payment chain |
| pay_address | string | Receiving address |
| usdt_amount | string | Expected USDT amount |
| actual_amount | string | Actual USDT paid on-chain |
| fiat_amount | number | Fiat amount |
| fiat_currency | string | Fiat currency code |
| tx_hash | string | On-chain transaction hash, globally unique |
| paid_at | string | On-chain payment time |
| attach | string | Merchant pass-through parameter (echoed back) |
| timestamp | int | Callback send timestamp |
| sign | string | Callback signature; verify with notify_secret |
Verify Callback Signature (PHP Example)
$notifySecret = 'YOUR_NOTIFY_SECRET'; // Obtain from API Settings in Merchant Console
$data = json_decode(file_get_contents('php://input'), true);
// Extract sign; remaining parameters participate in signature
$sign = $data['sign'];
unset($data['sign']);
// Sort by key ascending; notify_secret is HMAC key only
ksort($data);
$parts = [];
foreach ($data as $k => $v) {
if ($v === '' || $v === null) continue;
$parts[] = $k . '=' . $v;
}
$signStr = implode('&', $parts); // notify_secret is HMAC key only
$expected = hash_hmac('sha256', $signStr, $notifySecret);
if (!hash_equals($expected, $sign)) {
http_response_code(400);
exit('INVALID SIGN');
}
// Process business logic: update order status
if ((int)$data['status'] === 1) {
// TODO: Mark order as paid, fulfill order, grant access, etc.
updateOrderStatus($data['merchant_order_no'], 1);
}
echo 'OK'; // Must return OK or the system will retry
merchant_order_no or tx_hash for idempotency.
Verify Callback Signature (Helper Endpoint)
Submit received callback data to this endpoint for gateway-side verification of the notify_secret signature. Useful during debugging; in production, verify locally (see previous section).
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| sign | string | Required | Callback sign value to verify |
| (other callback fields) | mixed | Required | Pass all other fields from the full callback body as-is |
| api_key / timestamp / nonce / sign | — | Required | Common auth parameters (note naming conflicts with callback fields) |
Response Data
| Field | Type | Description |
|---|---|---|
| valid | bool | true = signature valid / false = signature invalid |
Order Status Codes
| Status | Meaning | Description |
|---|---|---|
| 0 Pending | Awaiting transfer | Order is valid; no on-chain payment detected |
| 1 Paid | Payment successful | On-chain payment confirmed; callback sent or pending |
| 2 Expired | Order expired | No payment received within validity period (default 30 minutes) |
| 3 Abnormal | Risk flagged | Risky transaction detected; requires manual review |
Error Codes
| HTTP Status | code | Common Causes |
|---|---|---|
| 400 | 400 | Missing or invalid parameters (e.g. negative amount, order ID too long) |
| 401 | 401 | Authentication failed: invalid api_key, bad signature, timestamp expired, nonce reused |
| 403 | 403 | Account disabled, authorization expired, or not activated (query and other fully authenticated endpoints; create order does not check authorization) |
| 404 | 404 | Order not found (or does not belong to current merchant) |
| 429 | 429 | Rate limit exceeded (default 60 requests/minute) |
| 500 | 500 | Internal server error; contact administrator |
Error Response Format
{
"code": 401,
"message": "Signature verification failed",
"data": null
}
SDK Downloads
Choose the SDK package for your platform, extract it, and follow the README to integrate. All packages include bundled dependencies — no extra installation required.
/wp-content/plugins/, then activate.upg folder in app/Payments/ and configure in the admin panel.FAQ
Q: How do I choose a payment chain?
Official WooCommerce, RiPro, and Dujiaoka plugins, plus the general SDK example, do not pass chain when creating orders — selection is handled on the UPG checkout page. Advanced API integrations may pass TRC20 / BEP20 / ERC20 to pre-lock a chain; if omitted, the default is auto-selected by TRC20→ERC20→BEP20 priority and users can still switch on checkout. TRC20 has the lowest fees and fastest confirmation.
Q: What if the user overpays or underpays?
The system identifies on-chain payments uniquely by tx_hash. If the actual amount does not match the expected amount, the order is marked as status 3 (Abnormal) and requires manual handling in the admin console.
Q: I never received a callback — what should I do?
1. Confirm notify_url is publicly reachable and returns OK; 2. Check that authorization/credits are valid; 3. Callbacks are usually sent automatically after on-chain payment — if missing, use Resend in Payment Records (manually marking paid does not trigger a callback); 4. If resend fails, check the error message returned.
Q: What happens if I submit a duplicate merchant order ID?
If the existing order is still pending and not expired, the API returns the original order data (idempotent) without creating a duplicate. If the old order has expired or been paid, a new order is created.
Q: Signature verification always fails — why?
Common causes: ① Empty-value fields included in the signature string (should be skipped); ② Number serialization adds extra decimals or spaces; ③ api_secret does not match api_key; ④ Server clock drift exceeds 5 minutes from standard time.
