请求示例和验签说明
请求方式:接口以 `GET` / `POST` 为主,请求体统一使用 `application/json`。
鉴权方式:开放接口通过请求头 + HMAC-SHA256 签名完成认证。
请求头
| 字段名称 | 字段类型 | 是否必填 | 说明 |
|---|---|---|---|
| x-api-key | string | 是 | 商户 API Key,使用商户后台 API 接入页面中的 mch_ 开头字符串。 |
| x-merchant-uid | string | 是 | 商户 UID。 |
| x-timestamp | string | 是 | 13 位毫秒时间戳,默认允许和服务器时间相差 5 分钟。 |
| x-nonce | string | 是 | 每次请求唯一的随机字符串,最长 128 位,重复使用会被拒绝。 |
| x-signature | string | 是 | 使用 HMAC-SHA256 生成的十六进制签名,签名原文由 Merchant UID、时间戳、nonce、请求方法、路径、规范化 query 和规范化 body 组成。 |
签名规则
- 请求体统一为 application/json;GET 接口没有 body 时,参与签名的 body 为空字符串。
- 签名路径必须是纯接口路径,例如 /openapi/payin/orders,不要带域名。
- query 会按 key 升序处理;body 会按 JSON key 升序后再序列化。
- 签名失败常见原因是时间戳过期、nonce 重复、字段顺序不一致或多余空格。
下面的固定时间戳、nonce 和凭据仅为离线签名对照值,不可用于实际请求。正式请求使用当前毫秒时间戳和新的随机 nonce。接口路径没有版本前缀;v1 仅指回调签名版本。
签名原文示例
880001
1776193200000
2f5c7b147c3748f0a8b3d9bb38aa91a4
POST
/openapi/payin/orders
{"amount":"100.00","chainCode":"TRON","merchantOrderNo":"M202604150001","notifyUrl":"https://merchant.example.com/api/uugate/payin-notify","tokenSymbol":"USDT"}完整请求示例
POST /openapi/payin/orders HTTP/1.1
Host: api.uugate.com
Content-Type: application/json
x-api-key: mch_xxxxxxxxxxxxxxxxxxxx
x-merchant-uid: 880001
x-timestamp: 1776193200000
x-nonce: 2f5c7b147c3748f0a8b3d9bb38aa91a4
x-signature: 6d7e96fdbf3ec37c1ec515540f46c6c19b08754973c43293e54644c2da910838
{
"chainCode": "TRON",
"tokenSymbol": "USDT",
"merchantOrderNo": "M202604150001",
"amount": "100.00",
"notifyUrl": "https://merchant.example.com/api/uugate/payin-notify"
}签名代码示例
- 四种语言使用相同的接口路径、参数和返回合同。示例从环境变量 UUGATE_MERCHANT_UID、UUGATE_API_KEY 读取凭据;UUGATE_BASE_URL 默认 https://api.uugate.com。
- 示例中的 canonicalBody 已按字段名排序并压缩为 JSON;修改参数后须递归排序对象字段,数组顺序保持,使用 UTF-8 且不额外转义斜杠或 Unicode。金额使用字符串,boolean 使用 true / false。
- 成功响应直接返回业务对象,没有统一的 code / data 外层。先判断 HTTP 状态,再处理正文;普通业务错误返回 code、message、data、requestId,安全拒绝可能只有 HTTP 401 / 403 和空正文。
- 网络超时或 HTTP 5xx 后先查询订单,不自动重复创建或提交付款;每次新请求重新生成 timestamp、nonce 和签名。
运行:Java 保存为 UUGateExample.java(回调为 UUGateCallback.java),使用 java 文件名;PHP 保存为 example.php,使用 php example.php;Go 保存为 example.go,使用 go run example.go;Node.js 保存为 example.mjs,使用 node example.mjs。回调原文从标准输入读取。
import { createHmac, randomBytes } from 'node:crypto';
function required(name) {
const value = process.env[name]?.trim();
if (!value) throw new Error(name + ' is required');
return value;
}
const uid = "880001", key = "mch_xxxxxxxxxxxxxxxxxxxx";
const timestamp = "1776193200000", nonce = "2f5c7b147c3748f0a8b3d9bb38aa91a4";
const method = 'POST', path = '/openapi/payin/orders';
const canonicalQuery = '', canonicalBody = "{\"amount\":\"100.00\",\"chainCode\":\"TRON\",\"merchantOrderNo\":\"M202604150001\",\"notifyUrl\":\"https://merchant.example.com/api/uugate/payin-notify\",\"tokenSymbol\":\"USDT\"}";
const payload = [uid, timestamp, nonce, method, path, canonicalQuery, canonicalBody].join('\n');
const signature = createHmac('sha256', key).update(payload, 'utf8').digest('hex');
console.log(signature);import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class UUGateExample {
static String required(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) throw new IllegalArgumentException(name + " is required");
return value.trim();
}
public static void main(String[] args) throws Exception {
String uid = "880001", key = "mch_xxxxxxxxxxxxxxxxxxxx";
String timestamp = "1776193200000", nonce = "2f5c7b147c3748f0a8b3d9bb38aa91a4";
String method = "POST", path = "/openapi/payin/orders";
String canonicalQuery = "", canonicalBody = "{\"amount\":\"100.00\",\"chainCode\":\"TRON\",\"merchantOrderNo\":\"M202604150001\",\"notifyUrl\":\"https://merchant.example.com/api/uugate/payin-notify\",\"tokenSymbol\":\"USDT\"}";
String payload = String.join("\n", uid, timestamp, nonce, method, path, canonicalQuery, canonicalBody);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = HexFormat.of().formatHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
System.out.println(signature);
}
}<?php
function required(string $name): string {
$value = trim((string) getenv($name));
if ($value === '') throw new RuntimeException($name . ' is required');
return $value;
}
$uid = "880001"; $key = "mch_xxxxxxxxxxxxxxxxxxxx";
$timestamp = "1776193200000"; $nonce = "2f5c7b147c3748f0a8b3d9bb38aa91a4";
$method = 'POST'; $path = '/openapi/payin/orders';
$canonicalQuery = ''; $canonicalBody = '{"amount":"100.00","chainCode":"TRON","merchantOrderNo":"M202604150001","notifyUrl":"https://merchant.example.com/api/uugate/payin-notify","tokenSymbol":"USDT"}';
$payload = implode("\n", [$uid, $timestamp, $nonce, $method, $path, $canonicalQuery, $canonicalBody]);
$signature = hash_hmac('sha256', $payload, $key);
echo $signature, PHP_EOL;package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"strings"
)
func required(name string) string {
value := strings.TrimSpace(os.Getenv(name))
if value == "" { panic(name + " is required") }
return value
}
func main() {
uid, key := "880001", "mch_xxxxxxxxxxxxxxxxxxxx"
timestamp, nonce := "1776193200000", "2f5c7b147c3748f0a8b3d9bb38aa91a4"
method, path := "POST", "/openapi/payin/orders"
canonicalQuery, canonicalBody := "", "{\"amount\":\"100.00\",\"chainCode\":\"TRON\",\"merchantOrderNo\":\"M202604150001\",\"notifyUrl\":\"https://merchant.example.com/api/uugate/payin-notify\",\"tokenSymbol\":\"USDT\"}"
payload := strings.Join([]string{uid, timestamp, nonce, method, path, canonicalQuery, canonicalBody}, "\n")
mac := hmac.New(sha256.New, []byte(key)); mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
fmt.Println(signature)
}普通业务错误返回 JSON:code、message、data、requestId。无效 Key、签名、时间戳、nonce 或安全准入拒绝可能返回 HTTP 401 / 403 和空正文,不保证有业务错误码。所有语言先检查 HTTP 状态及正文是否为空,再解析 JSON;message 可能随语言变化,不作为判断条件。
失败返回结构
| 字段名称 | 字段类型 | 是否必填 | 说明 |
|---|---|---|---|
| code | number | 是 | 业务错误码。 |
| message | string | 是 | 错误描述。 |
| data | object | null | 是 | 错误附加数据。 |
| requestId | string | null | 是 | 服务端请求追踪 ID。 |
失败返回示例
HTTP/1.1 401 Unauthorized
Cache-Control: no-store
Content-Length: 0