适用于自研系统的 RESTful JSON API,支持创建订单、查询状态、回调通知。端到端直付,资金不经平台,直达您的钱包。
Base URL:https://api2.188pay.top
Content-Type:所有请求与响应均使用 application/json
鉴权:请在商户后台的 API 密钥 页面获取 merchantId 和 secretKey
统一响应格式:{"code": 0, "msg": "success", "data": {...}} — code=0 成功,code=-1 失败
提交订单信息,获取收款钱包地址与实际应付金额。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchantId | string | 是 | 商户 ID |
| merchantOrderId | string | 是 | 商户订单号(您系统的唯一 ID) |
| amount | number | 是 | 订单金额(系统币种计价,如 CNY / USD / USDT) |
| coinType | string | 否 | 币种:usdt(默认)/ trx / fiat_alipay(支付宝收款)。建议显式传入;加密货币订单不传时签名仍按默认值 usdt 计算。 |
| notifyUrl | string | 否 | 回调地址(不填则使用钱包配置的地址) |
| returnUrl | string | 否 | 支付完成后跳转地址 |
| subject | string | 否 | 订单标题 |
| sign | string | 是 | MD5 签名(见 签名算法) |
curl -X POST https://api2.188pay.top/pay/address \
-H "Content-Type: application/json" \
-d '{
"merchantId": "YOUR_MERCHANT_ID",
"merchantOrderId": "ORDER_20260220001",
"amount": 100,
"coinType": "usdt",
"notifyUrl": "https://你的网站/callback",
"sign": "签名值"
}'
{
"code": 0,
"msg": "success",
"data": {
"orderId": "20260220143215123456",
"walletAddress": "TRx1234...abcd",
"actualAmount": 15.67,
"expireAt": "2026-02-20T14:52:15.123Z",
"cashierUrl": "https://api2.188pay.top/cashier?id=20260220143215123456"
}
}
| 字段 | 说明 |
|---|---|
| orderId | 平台订单号 |
| walletAddress | 收款钱包地址(引导用户转账到此地址) |
| actualAmount | 实际应付链上金额(含防碰撞微调,以此为准) |
| expireAt | 订单过期时间(ISO 8601 UTC) |
| cashierUrl | 托管收银台页面链接(可直接跳转展示给用户) |
{
"code": -1,
"msg": "错误信息"
}
根据平台订单号查询订单状态。此接口无需鉴权,为公开接口。
{
"code": 0,
"data": {
"id": "20260220143215123456",
"status": "CREATED",
"terminalReason": null,
"amount": 100,
"actualAmount": 15.67,
"coinType": "usdt",
"walletAddress": "TRx1234...abcd",
"expireAt": "2026-02-20T14:52:15.123Z",
"returnUrl": "https://你的网站/return"
}
}
| 状态值 | 说明 |
|---|---|
| CREATED | 等待支付中 |
| PROCESSING | 链上交易已匹配,等待确认 |
| SUCCESS | 链上已确认,支付成功 |
| FAILED | 支付失败 |
| CANCELED | 订单已终止(可通过 terminalReason 区分原因:EXPIRED / USER_CANCELED / ADMIN_CANCELED / CLOSED / REFUNDED / SYSTEM_FAILED) |
支付成功后,系统将以 POST JSON 方式向 notifyUrl 发送回调通知。
| 字段 | 类型 | 说明 |
|---|---|---|
| trade_no | string | 平台订单号 |
| out_trade_no | string | 商户订单号(创建时传入的 merchantOrderId) |
| amount | number | 订单原始金额 |
| actual_amount | number | 实际链上到账金额 |
| coin_type | string | 收款币种(usdt / trx) |
| status | string | 订单状态,回调时固定为 SUCCESS |
| tx_hash | string | null | 链上交易哈希;若值为 JSON null,验签时按字符串 null 参与签名 |
| sign | string | MD5 签名 |
| sign_type | string | 签名类型,固定为 MD5 |
处理要求:
out_trade_no 找到对应订单并处理业务逻辑success 或 ok(否则视为失败,触发重试)回调验签方式:排除 sign 和 sign_type 字段,其余参数只过滤空字符串后按 key 字母序排序,拼接为 k=v&k=v 格式,末尾追加 &key=YOUR_KEY,对整体做 MD5 取小写。JSON null 不过滤,按字符串 null 参与拼接,例如 tx_hash=null。注意:这是回调验签规则,创建订单的签名字段请看下方“下单签名字段”。
标准 JSON 模式签名。创建订单和回调验签的字段范围不同,请按对应场景处理。
下单签名字段:merchantId、merchantOrderId、amount、notifyUrl,以及有值时的 coinType、returnUrl。加密货币订单不传 coinType 时按 usdt 签名;如果法币下单使用 paymentMethod 字段,也需要参与签名。
不参与下单签名:subject、sign、sign_type、空字符串字段。
金额格式:签名里的 amount 必须和 JSON 请求中的原始值一致。例如请求传 100 就签 amount=100,不要改成 100.00。
amount=100&coinType=usdt&merchantId=YOUR_ID&merchantOrderId=ORDER_001¬ifyUrl=https://...
// 完整签名字符串
amount=100&coinType=usdt&merchantId=YOUR_ID&merchantOrderId=ORDER_001¬ifyUrl=https://...&key=YOUR_SECRET_KEY
// 结果
sign = MD5(完整字符串).toLowerCase()
标准模式 vs EPay 模式签名区别
标准模式末尾追加 &key=YOUR_KEY(含 &key= 前缀);EPay 兼容模式直接追加密钥本身(无任何前缀)。两者不可混用,请根据接入方式选择正确的签名方法。
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"sort"
"strings"
)
// Sign188PayOrder builds the signature for POST /pay/address.
// subject does not participate in order signing.
func Sign188PayOrder(req map[string]string, secretKey string) string {
signData := map[string]string{
"merchantId": req["merchantId"],
"merchantOrderId": req["merchantOrderId"],
"amount": req["amount"],
"notifyUrl": req["notifyUrl"],
}
if req["coinType"] != "" {
signData["coinType"] = req["coinType"]
} else if req["paymentMethod"] == "" {
signData["coinType"] = "usdt"
}
if req["returnUrl"] != "" {
signData["returnUrl"] = req["returnUrl"]
}
if req["paymentMethod"] != "" {
signData["paymentMethod"] = req["paymentMethod"]
}
return sign188PayStandard(signData, secretKey)
}
func sign188PayStandard(data map[string]string, secretKey string) string {
keys := make([]string, 0, len(data))
for k, v := range data {
if k == "sign" || k == "sign_type" {
continue
}
if v != "" {
keys = append(keys, k)
}
}
sort.Strings(keys)
var buffer bytes.Buffer
for i, k := range keys {
if i > 0 {
buffer.WriteString("&")
}
buffer.WriteString(k)
buffer.WriteString("=")
buffer.WriteString(data[k])
}
buffer.WriteString("&key=")
buffer.WriteString(strings.TrimSpace(secretKey))
hash := md5.Sum(buffer.Bytes())
return strings.ToLower(hex.EncodeToString(hash[:]))
}
$content = file_get_contents('php://input');
$data = json_decode($content, true);
$receivedSign = $data['sign'];
unset($data['sign'], $data['sign_type']);
$data = array_filter($data, function($v) { return $v !== ''; });
ksort($data);
$str = '';
foreach ($data as $k => $v) {
$value = $v === null ? 'null' : (string) $v;
$str .= $k . '=' . $value . '&';
}
$str = rtrim($str, '&');
$str .= '&key=YOUR_SECRET_KEY';
$sign = md5($str);
if ($receivedSign === $sign) {
echo 'success';
} else {
echo 'fail';
}
const crypto = require('crypto');
function verifyCallback(body, secretKey) {
const { sign, sign_type, ...params } = body;
const sortedStr = Object.keys(params)
.sort()
.filter(k => params[k] !== '')
.map(k => `${k}=${params[k]}`)
.join('&');
const computedSign = crypto
.createHash('md5')
.update(sortedStr + '&key=' + secretKey)
.digest('hex');
return computedSign === sign;
}
app.post('/callback', (req, res) => {
if (verifyCallback(req.body, 'YOUR_SECRET_KEY')) {
res.send('success');
} else {
res.send('fail');
}
});
import hashlib
def verify_callback(body: dict, secret_key: str) -> bool:
received_sign = body.pop('sign', '')
body.pop('sign_type', None)
filtered = {k: v for k, v in body.items() if v != ''}
stringify = lambda v: 'null' if v is None else str(v)
sorted_str = '&'.join(f'{k}={stringify(filtered[k])}' for k in sorted(filtered))
sign_str = sorted_str + '&key=' + secret_key
computed_sign = hashlib.md5(sign_str.encode()).hexdigest()
return computed_sign == received_sign
@app.route('/callback', methods=['POST'])
def callback():
data = request.get_json()
if verify_callback(data.copy(), 'YOUR_SECRET_KEY'):
return 'success'
return 'fail'
若回调接口未返回 success 或 ok,系统将按指数退避策略(min(15s × 2^(n-1), 30min))自动重发通知,最多重试 6 次。
| 次数 | 间隔 | 累计时间 |
|---|---|---|
| 1 | 15 秒后 | 约 15 秒 |
| 2 | 30 秒后 | 约 45 秒 |
| 3 | 1 分钟后 | 约 1.75 分钟 |
| 4 | 2 分钟后 | 约 3.75 分钟 |
| 5 | 4 分钟后 | 约 7.75 分钟 |
| 6 | 8 分钟后 | 约 15.75 分钟 |
全部 6 次重试失败后,可在商户后台订单详情页手动重发回调通知。