收款 API 文件

下載 Markdown 原始檔

支付 API 文件

1. 建立訂單 API

介面資訊

請求參數

參數名 類型 必填 說明
mid int 是 商戶 ID
orderid string 是 商戶訂單號,唯一標識
amount float 是 訂單金額(依系統設定之幣種,預設 TWD 新台幣)
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 錯誤訊息(僅失敗時回傳)

注意事項

  1. 訂單號唯一性:每個商戶的訂單號必須唯一。
  2. 金額精度:支援最多 2 位小數。
  3. 通知 URL:必須是可訪問的 HTTPS 地址。
  4. 金額調整:在交易繁忙時,系統會自動微調 USDT 支付金額(最多 +0.20)。
  5. 來源 IP 白名單:管理後台可為商戶設定收款 API IP 白名單;設定後,僅白名單中的 IP 可建立收款訂單。

2. 查詢訂單狀態 API

介面資訊

請求參數

參數名 類型 必填 說明
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_code=1 時為成功時間)
status string 回應狀態:ok-成功,error-失敗
message string 錯誤訊息(僅失敗時回傳)

注意事項

  1. 訂單查詢:只能查詢本商戶的訂單。
  2. 時間戳: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 或 MD5),詳見文件末尾簽章方法

通知回應要求

商戶收到通知後,必須回傳 HTTP 狀態碼 200,表示成功接收。如果回傳非 200 狀態碼,系統會認為通知失敗並進行重試。通知收妥後務請回應成功訊息,避免系統多次重試堵塞通道。

成功回應範例:

success

通知失敗重試機制

4. 簽章規則

商戶的簽章演算法由管理後台「商戶管理」中該商戶的設定決定,支援 RSA-SHA256 與 MD5 兩種;同一商戶所有請求與通知都使用相同的演算法。

RSA-SHA256 簽章

當商戶簽章演算法為 RSA 時:商戶在後台建立商戶資料時上傳 RSA 公鑰(PEM / PKIX),並自行安全保管配對私鑰。所有 API 請求由商戶私鑰使用 RSA-SHA256(PKCS#1 v1.5 + SHA-256)簽章;平台用該商戶公鑰驗簽。平台會自動生成自身金鑰對,後台「平台公鑰」介面回傳公鑰;商戶必須儲存該公鑰並用它驗證通知。

  1. 將除 sign 外的參數按參數名 ASCII 升序排序。
  2. 拼為 key=value&key=value,原始值不做 URL 編碼。
  3. 對 UTF-8 原文計算 SHA-256,並以 RSA PKCS#1 v1.5 簽章。
  4. 將簽章位元組作標準 Base64 編碼,放入 sign 參數。

簽章範例

原始參數:
mid=1001
orderid=ORDER123456
amount=100.00
notifyurl=https://example.com/notify

排序後:
amount=100.00&mid=1001&notifyurl=https://example.com/notify&orderid=ORDER123456

以上排序後的字串即簽章原文;用商戶 RSA 私鑰簽章並 Base64 編碼。

注意事項

MD5 簽章

當商戶簽章演算法為 MD5 時:後台「商戶管理」會為商戶產生一組 商戶金鑰(app_secret),商戶與平台共享此金鑰。所有 API 請求與通知都使用該金鑰計算 MD5 簽章;商戶端不需生成或上傳任何金鑰。

  1. 將除 sign 外的參數按參數名 ASCII 升序排序。
  2. 拼為 key=value&key=value,原始值不做 URL 編碼。
  3. 於字串末尾直接串接商戶金鑰,對 UTF-8 全文計算 MD5。
  4. 將 MD5 結果以大寫十六進位字串放入 sign 參數(驗證端大小寫不區分)。

MD5 簽章範例

原始參數:
mid=1001
orderid=ORDER123456
amount=100.00
notifyurl=https://example.com/notify

排序後:
amount=100.00&mid=1001&notifyurl=https://example.com/notify&orderid=ORDER123456

商戶金鑰 app_secret:ABC123...
簽章原文 = amount=100.00&mid=1001&notifyurl=https://example.com/notify&orderid=ORDER123456ABC123...
sign = strtoupper(md5(簽章原文))

MD5 簽章核心邏輯

$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)));
$params['sign'] = strtoupper(md5($source . $appSecret));
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, "&") + appSecret
params["sign"] = strings.ToUpper(fmt.Sprintf("%x", md5.Sum([]byte(source))))
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("&")) + appSecret;
MessageDigest md = MessageDigest.getInstance("MD5");
StringBuilder sb = new StringBuilder(); for (byte b : md.digest(source.getBytes(StandardCharsets.UTF_8))) sb.append(String.format("%02X", b));
params.put("sign", sb.toString());

MD5 通知驗簽核心邏輯

通知收到後,將 sign 取出並從參數集合移除;其餘參數按同樣規則排序拼接後串接商戶金鑰計算 MD5,與回傳的 sign(不分大小寫)比對。

$signature = strtoupper($params['sign']); unset($params['sign']); ksort($params, SORT_STRING);
$source = implode('&', array_map(fn($k) => $k.'='.$params[$k], array_keys($params)));
$valid = strtoupper(md5($source . $appSecret)) === $signature;
signature := strings.ToUpper(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]) }
source := strings.Join(parts, "&") + appSecret
valid := strings.ToUpper(fmt.Sprintf("%x", md5.Sum([]byte(source)))) == signature
String signature = params.remove("sign").toUpperCase();
String source = params.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(e -> e.getKey()+"="+e.getValue()).collect(Collectors.joining("&")) + appSecret;
MessageDigest md = MessageDigest.getInstance("MD5");
StringBuilder sb = new StringBuilder(); for (byte b : md.digest(source.getBytes(StandardCharsets.UTF_8))) sb.append(String.format("%02X", b));
boolean valid = sb.toString().equals(signature);

注意:MD5 為共享金鑰簽章,商戶金鑰等同密碼,請透過安全管道傳遞並妥善保管,切勿於前端或公開場所洩露;後台重新產生金鑰後,舊金鑰立即失效。

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()));

RSA 通知驗簽核心邏輯

通知收到後,將 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);