标准 JSON API · REST

188Pay API 接入文档

适用于自研系统的 RESTful JSON API,支持创建订单、查询状态、回调通知。端到端直付,资金不经平台,直达您的钱包。

BASE URL: api2.188pay.top
Content-Type: application/json
签名算法: MD5
回调重试: 最多 6 次

Base URL:https://api2.188pay.top

Content-Type:所有请求与响应均使用 application/json

鉴权:请在商户后台的 API 密钥 页面获取 merchantIdsecretKey

统一响应格式:{"code": 0, "msg": "success", "data": {...}} — code=0 成功,code=-1 失败

创建订单 POST /pay/address

提交订单信息,获取收款钱包地址与实际应付金额。

参数 类型 必填 说明
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
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": "签名值"
  }'
JSON
{
  "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 托管收银台页面链接(可直接跳转展示给用户)
JSON
{
  "code": -1,
  "msg": "错误信息"
}

查询订单 GET /pay/order/{orderId}

根据平台订单号查询订单状态。此接口无需鉴权,为公开接口。

JSON
{
  "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

处理要求:

  1. 验证签名是否正确(防止伪造回调)
  2. 根据 out_trade_no 找到对应订单并处理业务逻辑
  3. 业务处理成功后响应字符串 successok(否则视为失败,触发重试)

回调验签方式:排除 signsign_type 字段,其余参数只过滤空字符串后按 key 字母序排序,拼接为 k=v&k=v 格式,末尾追加 &key=YOUR_KEY,对整体做 MD5 取小写。JSON null 不过滤,按字符串 null 参与拼接,例如 tx_hash=null。注意:这是回调验签规则,创建订单的签名字段请看下方“下单签名字段”。

签名算法

标准 JSON 模式签名。创建订单和回调验签的字段范围不同,请按对应场景处理。

下单签名字段:merchantIdmerchantOrderIdamountnotifyUrl,以及有值时的 coinTypereturnUrl。加密货币订单不传 coinType 时按 usdt 签名;如果法币下单使用 paymentMethod 字段,也需要参与签名。

不参与下单签名:subjectsignsign_type、空字符串字段。

金额格式:签名里的 amount 必须和 JSON 请求中的原始值一致。例如请求传 100 就签 amount=100,不要改成 100.00

  1. 步骤 1 — 过滤空字符串,按 key 字母序排序后拼接参数串
    示例
    amount=100&coinType=usdt&merchantId=YOUR_ID&merchantOrderId=ORDER_001&notifyUrl=https://...
  2. 步骤 2 — 末尾追加 &key=YOUR_SECRET_KEY,对完整字符串做 MD5 取小写
    示例
    // 完整签名字符串
    amount=100&coinType=usdt&merchantId=YOUR_ID&merchantOrderId=ORDER_001&notifyUrl=https://...&key=YOUR_SECRET_KEY
    
    // 结果
    sign = MD5(完整字符串).toLowerCase()

标准模式 vs EPay 模式签名区别

标准模式末尾追加 &key=YOUR_KEY(含 &key= 前缀);EPay 兼容模式直接追加密钥本身(无任何前缀)。两者不可混用,请根据接入方式选择正确的签名方法。

Go 下单签名
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[:]))
}
PHP 回调验签
$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';
}
Node.js 回调验签
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');
  }
});
Python 回调验签
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'

重试策略

若回调接口未返回 successok,系统将按指数退避策略(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 次重试失败后,可在商户后台订单详情页手动重发回调通知。