AI Integration Guide

This is a general integration guide that can be copied directly into an AI coding assistant. It covers merchant OpenAPI authentication, signing, callback verification, status handling, and the current public endpoint list.

Replace baseUrl, Merchant UID, API Key, and business callback URLs with merchant values, then ask the AI to generate an SDK or integration code in the target language.

Authenticate the merchant with Merchant UID and the API Key beginning with mch_.
Covers pay-in, exclusive wallets, single payout, and callbacks.
Includes signing rules, endpoint list, field constraints, error codes, and an implementation checklist.

Full Markdown Document

# UUGate OpenAPI AI 通用对接文档

把这份文档直接交给 AI 编程助手,让它按你的技术栈实现 UUGate 商户 OpenAPI 对接。实现时以本文档为规格来源。

## 1. 接入目标

实现一个可复用的 UUGate 商户接入层,至少包含:

1. 统一配置:baseUrl、merchantUid、apiKey。
2. HMAC-SHA256 请求签名和带签名请求头的 HTTP Client。
3. 收款订单:创建、查询。
4. 独享钱包:创建、查询详情。
5. 代付订单:创建、查询、提交。
6. 回调接收接口:使用 apiKey 验签,按 status 幂等更新本地业务订单。
7. 错误码处理、幂等处理、金额字符串处理和日志追踪。

## 2. 基础变量

请把下面变量替换成真实值:

~~~env
UUGATE_BASE_URL=https://api.uugate.com
UUGATE_MERCHANT_UID=880001
UUGATE_API_KEY=mch_xxxxxxxxxxxxxxxxxxxx
UUGATE_PAYIN_NOTIFY_URL=https://merchant.example.com/api/uugate/payin-notify
UUGATE_PAYOUT_NOTIFY_URL=https://merchant.example.com/api/uugate/payout-notify
~~~

说明:

- baseUrl 是 API 域名,不要把接口 path 拼进 baseUrl。
- Merchant UID 是商户唯一身份。
- API Key 是商户后台 API 接入里的 mch_ 开头字符串,同时也是开放接口签名密钥。
- API Key 同时用于开放接口请求签名与回调验签;回调必须使用创建该订单时的密钥。
- notifyUrl 必须是商户自己可公网访问的回调地址;live 环境建议使用 HTTPS。

## 3. 支持币种和链路

当前业务只支持以下链路和币种组合:

| chainCode | tokenSymbol | 业务币种 |
| --- | --- | --- |
| TRON | USDT | TRC20-USDT |
| BSC | USDT | BEP20-USDT |
| BSC | USDC | BEP20-USDC |
| SOL | USDC | SPL-USDC |

金额一律使用字符串传输,例如 100.00 或 35.500000。不要用浮点数参与金额计算;内部建议使用 Decimal / BigNumber。

## 4. 请求鉴权和签名

所有 /openapi/ 接口都需要以下请求头:

| Header | 必填 | 说明 |
| --- | --- | --- |
| x-api-key | 是 | 商户 API Key,mch_ 开头 |
| x-merchant-uid | 是 | Merchant UID |
| x-timestamp | 是 | 13 位毫秒时间戳,默认允许和服务器时间相差 5 分钟 |
| x-nonce | 是 | 每次请求唯一随机串,最长 128 位,不能重复 |
| x-signature | 是 | HMAC-SHA256 十六进制签名 |
| content-type | POST 必填 | application/json |

签名原文固定为 7 行,用换行符 \n 拼接:

~~~text
merchantUid
timestamp
nonce
method
path
canonicalQuery
canonicalBody
~~~

字段规则:

- merchantUid、timestamp、nonce 都先 trim。
- method 使用大写,例如 GET、POST。
- path 只使用接口路径,例如 /openapi/payin/orders,不带协议、域名和 query。
- canonicalQuery:把 query 对象按 key 升序;空字符串、null、undefined 跳过;数组逐项展开;最后按 key、value 排序并 URL encode,格式为 a=1&b=2。
- canonicalBody:POST JSON body 按 key 升序递归规范化后 JSON.stringify;数组顺序保持不变;undefined 字段删除;Date 转 ISO 字符串;GET 无 body 时为空字符串。
- 签名算法:HMAC-SHA256(secret=apiKey, message=签名原文),输出 lowercase hex。
- 发送请求时的 JSON body 必须和参与签名的 body 保持一致。

Node.js 签名参考实现:

~~~js
import crypto from 'node:crypto';

function normalizePath(path) {
  const value = String(path || '').trim();
  return value.startsWith('/') ? value : '/' + value;
}

function normalizeJson(value) {
  if (value === undefined) return undefined;
  if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
  if (Array.isArray(value)) return value.map((item) => {
    const normalized = normalizeJson(item);
    return normalized === undefined ? null : normalized;
  });
  if (value instanceof Date) return value.toISOString();
  if (typeof value === 'object') {
    const output = {};
    for (const key of Object.keys(value).sort()) {
      const normalized = normalizeJson(value[key]);
      if (normalized !== undefined) output[key] = normalized;
    }
    return output;
  }
  return String(value);
}

function canonicalBody(body) {
  const normalized = normalizeJson(body);
  return normalized === undefined ? '' : JSON.stringify(normalized);
}

function canonicalQuery(query) {
  if (!query || typeof query !== 'object' || Array.isArray(query)) return '';
  const entries = [];
  for (const key of Object.keys(query).sort()) {
    const raw = query[key];
    if (raw === undefined || raw === null || raw === '') continue;
    const values = Array.isArray(raw) ? raw : [raw];
    for (const item of values) {
      if (item === undefined || item === null) continue;
      const value = typeof item === 'object' ? canonicalBody(item) : String(item);
      entries.push([key, value]);
    }
  }
  return entries
    .sort((a, b) => a[0] === b[0] ? a[1].localeCompare(b[1]) : a[0].localeCompare(b[0]))
    .map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
    .join('&');
}

export function buildSignedHeaders({ merchantUid, apiKey, method, path, query, body }) {
  const timestamp = String(Date.now());
  const nonce = crypto.randomBytes(16).toString('hex');
  const payload = [
    String(merchantUid).trim(),
    timestamp,
    nonce,
    String(method).trim().toUpperCase(),
    normalizePath(path),
    canonicalQuery(query),
    canonicalBody(body),
  ].join('\n');

  const signature = crypto.createHmac('sha256', String(apiKey).trim()).update(payload).digest('hex');
  return {
    'x-api-key': String(apiKey).trim(),
    'x-merchant-uid': String(merchantUid).trim(),
    'x-timestamp': timestamp,
    'x-nonce': nonce,
    'x-signature': signature,
  };
}
~~~

## 5. 响应结构

成功响应:HTTP 2xx,body 是接口自己的 JSON 对象,不包一层 code。

失败响应通常为:

~~~json
{
  "code": 20011,
  "message": "Invalid API signature",
  "data": null,
  "requestId": "9e0cf688-11fd-4cd2-83aa-61df77123456"
}
~~~

客户端需要记录 requestId,方便排查。

## 6. OpenAPI 接口清单

### 6.1 创建收款订单

POST /openapi/payin/orders

请求 body:

~~~json
{
  "chainCode": "TRON",
  "tokenSymbol": "USDT",
  "merchantOrderNo": "M202604150001",
  "amount": "100.00",
  "notifyUrl": "https://merchant.example.com/api/uugate/payin-notify"
}
~~~

字段:

- chainCode: TRON / BSC / SOL。
- tokenSymbol: USDT / USDC,必须和 chainCode 匹配。
- merchantOrderNo: 商户订单号,最长 128。
- amount: 收款金额字符串。
- notifyUrl: 本订单回调地址,必填。

响应重点字段:

- orderNo: 平台收款订单号。
- merchantOrderNo: 商户订单号。
- amount: 订单金额。
- paidAmount: 实收金额,未支付时为 null。
- address: 实际收款地址。
- cashierUrl: 收银台完整地址,商户前端可以直接跳转。
- paymentUri: 钱包拉起 URI。
- expireAt / createdAt / detectedAt / confirmedAt / completedAt。
- status: waiting_payment / detected / confirming / completed / risk_settled / expired / frozen_abnormal / cancelled。

### 6.2 查询收款订单

GET /openapi/payin/orders/{orderNo}

path 参数:

- orderNo: 创建收款订单返回的平台订单号。

响应字段和创建收款订单一致。业务最终成功建议以 status=completed 为准。

### 6.3 创建独享钱包

POST /openapi/payin/exclusive-bindings

用于给会员、站点、业务账号等 bindKey 创建长期专属收款地址。

请求 body:

~~~json
{
  "chainCode": "TRON",
  "tokenSymbol": "USDT",
  "bindKey": "USER_90001",
  "label": "VIP User 90001",
  "notifyUrl": "https://merchant.example.com/api/uugate/member-topup-notify"
}
~~~

字段:

- bindKey: 商户侧唯一绑定键,最长 128。
- label: 备注,选填。
- notifyUrl: 专属地址后续自动生成充值订单时优先使用的回调地址,选填。

响应重点字段:

- bindingId、chainCode、tokenSymbol、bindKey、addressId、address、addressType=exclusive、notifyUrl、status。
- 同一 bindKey 重复创建时应按 get-or-create 思路处理,优先复用平台返回的已有地址。

### 6.4 查询独享钱包详情

GET /openapi/payin/exclusive-bindings/{bindKey}?chainCode=TRON&tokenSymbol=USDT&recentLimit=10

query 参数:

- chainCode: 必填,TRON / BSC / SOL。
- tokenSymbol: 必填,USDT / USDC。
- recentLimit: 选填,1 到 20,默认 10。

响应重点字段:

- bindingId、chainCode、tokenSymbol、bindKey、label、addressId、address、addressType、notifyUrl、status。
- stats: totalOrders、completedOrders、totalPaidAmount、completedPaidAmount、lastOrderAt、lastCompletedAt。
- recentOrders: 最近自动充值订单列表。
- recentTransactions: 最近链上入账交易列表。

### 6.5 创建单笔代付订单

公开 API 只提供单笔付款;批量付款仅在商户后台使用。

POST /openapi/payout/orders

请求 body:

~~~json
{
  "chainCode": "TRON",
  "tokenSymbol": "USDT",
  "fromAddress": "TNjBrNq2a9FK2vwu4W9C1QdLKSZ42Yx5YY",
  "toAddress": "TS7b7iD8G2PaPqK1TqSmLJ9nrrYH4oKX1S",
  "amount": "35.50",
  "deleteSourceAfterSuccess": false,
  "merchantOrderNo": "PO202604150001",
  "notifyUrl": "https://merchant.example.com/api/uugate/payout-notify"
}
~~~

字段:

- fromAddress: 指定付款地址,选填;不传时系统自动选择满足余额条件的可用地址。
- toAddress: 收款地址,必须与 chainCode 地址格式匹配,不能是商户自己的钱包地址。
- amount: 代付金额字符串。来源可用余额足够付款金额即可,不强制预留余额,付款金额不会自动扣减。
- deleteSourceAfterSuccess: 可选 JSON boolean,默认 false;不接受字符串。传 true 时在链上确认成功后软删除来源地址;失败不删除。历史订单、账目及密钥引用保留。有其他未完成转账时不能选择删除,同一商户订单号重试必须保持该参数一致。
- merchantOrderNo: 商户侧订单号,最长 128。
- notifyUrl: 本订单回调地址,必填。

响应重点字段:

- orderNo、merchantOrderNo、fromAddress、toAddress、amount、txHash、status、submittedAt、broadcastedAt、confirmedAt、failedAt、createdAt。
- 创建成功后系统会尝试自动提交链上;status 可能是 submitted、broadcasted、confirmed 或 failed。
- 创建成功不等于链上成功,业务终态以 confirmed / failed 或查询结果为准。

### 6.6 查询单笔代付订单

GET /openapi/payout/orders/{orderNo}

响应字段同创建单笔代付订单。业务最终成功以 status=confirmed 为准,失败以 status=failed 为准。

### 6.7 提交单笔代付订单

POST /openapi/payout/orders/{orderNo}/submit

请求 body:

~~~json
{
  "remark": "manual submit to signer gateway"
}
~~~

仅当订单仍为 submitted 时可提交。创建接口已经会自动尝试提交,所以通常只在自动提交失败后用于重试。

## 7. 回调验签

平台向 notifyUrl 或商户默认 callbackUrl 发起 POST application/json 回调。

回调请求头:

| Header | 说明 |
| --- | --- |
| x-callback-timestamp | 毫秒时间戳 |
| x-callback-nonce | 回调随机串 |
| x-callback-signature | HMAC-SHA256(apiKey, timestamp + "." + nonce + "." + rawBody) |
| x-callback-signature-algorithm | HMAC-SHA256 |
| x-callback-signature-version | v1 |

验签规则:

1. 必须使用 HTTP 原始请求体 rawBody,不要用重新序列化后的 JSON 字符串代替。
2. 用 apiKey 做 HMAC-SHA256。
3. digest 输出 lowercase hex 后和 x-callback-signature 做常量时间比较。
4. 验签通过后再更新业务订单。
5. 返回任意 HTTP 2xx 表示成功并立即停止;非 2xx 或超时会在 1 分钟后进行第 2 次投递,再失败则在 5 分钟后进行第 3 次投递,总计最多 3 次。

Node.js 回调验签参考:

~~~js
import crypto from 'node:crypto';

export function verifyUUGateCallback({ apiKey, timestamp, nonce, signature, rawBody }) {
  const expected = crypto
    .createHmac('sha256', String(apiKey).trim())
    .update(String(timestamp).trim() + '.' + String(nonce).trim() + '.' + rawBody)
    .digest('hex');

  const actual = String(signature || '').trim().toLowerCase();
  if (!actual || actual.length !== expected.length) return false;
  return crypto.timingSafeEqual(Buffer.from(actual, 'utf8'), Buffer.from(expected, 'utf8'));
}
~~~

回调 body 会包含 merchantUid 和业务字段。常见收款回调字段:

~~~json
{
  "merchantUid": "880001",
  "orderNo": "PI1776193200123ABCD1234",
  "merchantOrderNo": "M202604150001",
  "mode": "shared",
  "businessType": "fixed",
  "bindKey": "USER_90001",
  "expectedAmount": "100.000000",
  "paidAmount": "100.000000",
  "chainCode": "TRON",
  "tokenSymbol": "USDT",
  "txHash": "f7f17891f52c35d0c93170f0d12bb347ac16ab34853b4bc8dcf7fd0a8c9aa321",
  "fromAddress": "TS7b7iD8G2PaPqK1TqSmLJ9nrrYH4oKX1S",
  "status": "completed"
}
~~~

收款状态处理:

- detected / confirming 是过程状态。
- completed 是最终成功。
- risk_settled 表示有风险结算,按商户风控策略处理。
- 回调可能重复投递,必须按 orderNo 或 merchantOrderNo 幂等更新。

常见代付回调字段:

~~~json
{
  "merchantUid": "880001",
  "orderNo": "PO1776193200A1B2C3D4",
  "merchantOrderNo": "PO202604150001",
  "chainCode": "TRON",
  "tokenSymbol": "USDT",
  "fromAddress": "TNjBrNq2a9FK2vwu4W9C1QdLKSZ42Yx5YY",
  "toAddress": "TS7b7iD8G2PaPqK1TqSmLJ9nrrYH4oKX1S",
  "txHash": "b3d4fb0cb11eb86bb906d1e0ee3b84e17f40d5441d1e4c651ea46920a48a9a01",
  "amount": "35.500000",
  "status": "confirmed"
}
~~~

代付状态处理:

- submitted / broadcasted 是过程状态。
- confirmed 是最终成功。
- failed 是最终失败,failedStage 可能说明失败阶段。

## 8. 常见错误码

| code | 说明 |
| --- | --- |
| 10001 | 请求参数不合法 |
| 20005 | API Key 无效 |
| 20011 | 请求签名无效 |
| 20012 | 签名时间戳超出允许窗口 |
| 20016 | nonce 已被使用,请勿重放请求 |
| 31001 | 链不支持 |
| 31002 | 代币不支持 |
| 31003 | 链和代币不匹配 |
| 40002 | 独享钱包绑定不存在 |
| 40105 | 共享收款金额已被占用 |
| 40108 | 收款订单不存在 |
| 51002 | 商户运营状态、操作开关、限额或费用余额导致操作受限 |
| 51004 | 费用余额不足 |
| 53001 | 来源地址不存在 |
| 53002 | 来源地址不允许用于当前操作 |
| 53003 | 币种可用余额不足 |
| 53005 | 来源地址对应链和币种余额不足 |
| 54001 | 代付订单不存在 |
| 54003 | 不能付款给商户自己的地址 |

## 9. 实现要求和检查清单

请 AI 生成代码时遵守:

1. 使用 Merchant UID + API Key 完成商户鉴权。
2. 所有金额都用字符串和 Decimal,不用 float。
3. 每个请求都生成新的 timestamp 和 nonce。
4. 签名 path 永远不带域名和 query。
5. GET 请求 body 参与签名时为空字符串。
6. POST 请求签名使用规范化 body,并发送同一个 body。
7. query 参数参与签名前要排序和 URL encode。
8. 回调必须先验签,再做业务处理。
9. 回调处理必须幂等:同一个 orderNo、txHash 或 status 重复到达不能重复加款或重复扣款。
10. 收款最终成功只认 completed;代付最终成功只认 confirmed。
11. 对 20011、20012、20016 不要盲目重试,应重新生成签名请求。
12. 对网络错误或 5xx 可以做有限重试,但每次重试必须重新签名。
13. 日志里不要打印完整 API Key 。

## 10. 推荐 SDK 方法命名

建议生成以下方法:

~~~text
createPayinOrder(payload)
getPayinOrder(orderNo)
createExclusiveBinding(payload)
getExclusiveBinding(bindKey, query)
createPayoutOrder(payload)
getPayoutOrder(orderNo)
submitPayoutOrder(orderNo, payload)
verifyCallback(headers, rawBody)
~~~

完成后至少写 3 个测试:

1. 签名 payload 构造测试:字段顺序、query 排序、body key 排序。
2. 回调验签测试:rawBody 不变时成功,body 被重新格式化时失败。
3. 业务幂等测试:同一个 completed 或 confirmed 回调重复投递不会重复更新余额或订单。