支付API文檔
1. 創建訂單 API
介面訊息
- URL:
/pay/create - 方法:
POST - Content-Type:
application/x-www-form-urlencoded
請求參數
| 參數名 | 類型 | 必填 | 說明 |
|---|---|---|---|
| mid | int | 是 | 商戶ID |
| orderid | string | 是 | 商戶訂單號,唯一標識 |
| amount | float | 是 | 訂單金額(法幣) |
| notifyurl | string | 是 | 支付結果通知URL |
| sign | string | 是 | 簽章,請參考文檔末尾簽章方法 |
回應格式
成功回應
{
"status": "ok",
"orderid": "ORDER123456",
"address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
"uamount": "0.12",
"url": "https://demosite.com/pay?id=abc123def456"
}
錯誤回應
{
"status": "error",
"message": "系統繁忙,請稍後再試!"
}
回應欄位說明
| 欄位名 | 類型 | 說明 |
|---|---|---|
| status | string | 回應狀態:ok-成功,error-失敗 |
| orderid | string | 商戶訂單號 |
| address | string | 錢包地址 |
| uamount | string | 需要支付的USDT數量 |
| url | string | 支付頁面URL,請將使用者浏覽器跳轉到此地址。或者商戶自行展示上述錢包和金額訊息。 |
| message | string | 錯誤訊息(僅失敗時回傳) |
注意事項
- 訂單號唯一性:每個商戶的訂單號必須唯一。
- 金額精度:支援最多2位小數
- 通知URL:必須是可訪问的HTTPS地址
- 金額調整:在交易繁忙時,系統會自動調整USDT支付金額(最多+0.09)
- 來源 IP 白名單:管理後台可為商戶設定收款 API IP 白名單;設定後,僅白名單中的 IP 可建立收款訂單。
2. 查詢訂單狀態 API
介面訊息
- URL:
/pay/checkorder - 方法:
POST - Content-Type:
application/x-www-form-urlencoded
請求參數
| 參數名 | 類型 | 必填 | 說明 |
|---|---|---|---|
| mid | string | 是 | 商戶ID |
| orderid | string | 是 | 商戶訂單號 |
| sign | string | 是 | 簽章,請參考文檔末尾簽章方法 |
回應格式
成功回應
{
"status": "ok",
"orderid": "ORDER123456",
"status_code": 0,
"amount": 100.00,
"uamount": 0.12,
"updatetime": 1640995200
}
錯誤回應
{
"status": "error",
"message": "訂單不存在"
}
回應欄位說明
| 欄位名 | 類型 | 說明 |
|---|---|---|
| orderid | string | 商戶訂單號 |
| status_code | int | 訂單狀態:0-未支付,1-已支付 |
| amount | float | 訂單金額(法幣) |
| uamount | float | USDT金額 |
| updatetime | int | 更新時間戳(status_code=0時為創建時間,status=1時為成功時間) |
| status | string | 回應狀態:ok-成功,error-失敗 |
| message | string | 錯誤訊息(僅失敗時回傳) |
注意事項
- 訂單查詢:只能查詢本商戶的訂單
- 時間戳:updatetime欄位根據訂單狀態回傳不同時間
- status=0:回傳訂單創建時間
- status=1:回傳訂單成功時間
3. 支付回調通知
回調機制說明
當訂單支付成功時,系統會主動向商戶提供的notifyurl發送POST請求通知支付結果。
回調參數
| 參數名 | 類型 | 說明 |
|---|---|---|
| orderid | string | 商戶訂單號 |
| amount | float | 訂單金額(法幣) |
| uamount | float | USDT支付金額 |
| created_time | int | 訂單創建時間戳 |
| success_time | int | USDT轉帳時間 |
| sign | string | 平台以 RSA-SHA256 生成的 Base64 簽章 |
回調回應要求
商戶收到回調後,必須回傳HTTP狀態碼200,表示成功接收。如果回傳非200狀態碼,系統會认為回調失敗並進行重試。 通知收妥後務請回應成功訊息,避免系統徒勞的多次重試堵塞通道。
成功回應示例:
success
回調失敗重試機制
- 重試次數:最多重試9次
- 重試間隔:採用指數退避算法
- 第1次失敗:30秒後重試
- 第2次失敗:60秒後重試
- 第3次失敗:120秒後重試
- 第4次及以後:2^(失敗次數-1) * 60秒
- 逾時处理:超過9次失敗後,系統會放棄此訂單的通知。
4. 簽章規則
RSA-SHA256 簽章
商戶在後台創建商戶資料時上傳 RSA 公鑰(PEM / PKIX),並自行安全保管配對私鑰。所有 API 請求由商戶私鑰使用 RSA-SHA256(PKCS#1 v1.5 + SHA-256)簽章;平台用該商戶公鑰驗簽。平台會自動生成自身金鑰對,後台“平台公鑰”介面回傳公鑰;商戶必須儲存該公鑰並用它驗證回調。
- 將除
sign外的參數按參數名 ASCII 升序排序。 - 拼為
key=value&key=value,原始值不做 URL 编碼。 - 對 UTF-8 原文計算 SHA-256,並以 RSA PKCS#1 v1.5 簽章。
- 將簽章字節作標準 Base64 编碼,放入
sign參數。
簽章示例
原始參數:
mid=1001
orderid=ORDER123456
amount=100.00
notifyurl=https://example.com/notify
排序後:
amount=100.00&mid=1001¬ifyurl=https://example.com/notify&orderid=ORDER123456
以上排序後的字符串即簽章原文;用商戶 RSA 私鑰簽章並 Base64 编碼。
注意事項
- 所有API介面都需要使用相同的簽章規則
- 簽章驗證失敗會導致請求被拒絕
- 請妥善保管商戶 RSA 私鑰,絕不可上傳或洩露;後台只儲存公鑰。
簽章核心邏輯
以下示例先用實際業務欄位構建請求參數與簽章原文,privateKey 為商戶私鑰;將得到的 sign 放回參數集合後送出。回調驗簽時改用平台公鑰和對應語言的驗簽 API。
$params = ['mid'=>'1001', 'orderid'=>'ORDER123456', 'amount'=>'100.00', 'notifyurl'=>'https://example.com/notify'];
ksort($params, SORT_STRING);
$source = implode('&', array_map(fn($k) => $k.'='.$params[$k], array_keys($params)));
openssl_sign($source, $raw, $privateKey, OPENSSL_ALGO_SHA256);
$params['sign'] = base64_encode($raw);
params := map[string]string{"mid":"1001", "orderid":"ORDER123456", "amount":"100.00", "notifyurl":"https://example.com/notify"}
keys := make([]string, 0, len(params)); for k := range params { keys = append(keys, k) }; sort.Strings(keys)
parts := make([]string, 0, len(keys)); for _, k := range keys { parts = append(parts, k+"="+params[k]) }; source := strings.Join(parts, "&")
hash := sha256.Sum256([]byte(source))
raw, _ := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
params["sign"] = base64.StdEncoding.EncodeToString(raw)
Map<String,String> params = new TreeMap<>();
params.put("mid", "1001"); params.put("orderid", "ORDER123456"); params.put("amount", "100.00"); params.put("notifyurl", "https://example.com/notify");
String source = params.entrySet().stream().map(e -> e.getKey()+"="+e.getValue()).collect(Collectors.joining("&"));
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey); signer.update(source.getBytes(StandardCharsets.UTF_8));
params.put("sign", Base64.getEncoder().encodeToString(signer.sign()));
回調驗簽核心邏輯
回調收到後,將 sign 取出並從參數集合移除;其餘參數按同樣規則排序拼接。platformPublicKey 為後台展示的平台公鑰。
$signature = base64_decode($params['sign']); unset($params['sign']); ksort($params, SORT_STRING);
$source = implode('&', array_map(fn($k) => $k.'='.$params[$k], array_keys($params)));
$valid = openssl_verify($source, $signature, $platformPublicKey, OPENSSL_ALGO_SHA256) === 1;
signature, _ := base64.StdEncoding.DecodeString(params["sign"]); delete(params, "sign")
keys := make([]string, 0, len(params)); for k := range params { keys = append(keys, k) }; sort.Strings(keys)
parts := make([]string, 0, len(keys)); for _, k := range keys { parts = append(parts, k+"="+params[k]) }; digest := sha256.Sum256([]byte(strings.Join(parts, "&")))
err := rsa.VerifyPKCS1v15(platformPublicKey, crypto.SHA256, digest[:], signature)
byte[] signature = Base64.getDecoder().decode(params.remove("sign"));
String source = params.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(e -> e.getKey()+"="+e.getValue()).collect(Collectors.joining("&"));
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(platformPublicKey); verifier.update(source.getBytes(StandardCharsets.UTF_8));
boolean valid = verifier.verify(signature);