接入指南

PayHub 是多租户支付网关。商户在自己的控制台里绑定支付宝、微信支付或持牌聚合通道,平台负责下单、收银台、 异步通知与退款编排,资金直接进入商户自己的上游账户。

合规提示:本系统不提供代收代付、资金归集与结算能力。在中国境内经营支付业务需要《支付业务许可证》, 请确保你的业务角色(商户 / 服务商)与实际持牌情况一致。

1. 认证与签名

所有 /api/v1 接口使用 HMAC-SHA256 请求签名,请求头必须包含:

请求头说明
X-Api-Key商户 App Key,在控制台「开发者设置」查看
X-TimestampUnix 时间戳(秒或毫秒),与服务器偏差需 ≤ 5 分钟
X-Nonce随机串,同一 nonce 只能使用一次(防重放)
X-Signature下方规则算出的 64 位小写十六进制签名

签名串由 5 段组成,用换行符连接:

METHOD            // 大写,如 POST
PATH              // 不含域名与查询串,如 /api/v1/orders
TIMESTAMP         // 与 X-Timestamp 完全一致的原始字符串
NONCE             // 与 X-Nonce 一致
SHA256_HEX(BODY)  // 原始请求体(GET 为空字符串)的 SHA-256 十六进制

然后 signature = HMAC_SHA256_HEX(secret, 上面拼好的字符串)。

import crypto from "node:crypto";

const APP_KEY = process.env.PAYHUB_APP_KEY;
const APP_SECRET = process.env.PAYHUB_APP_SECRET;
const BASE = "https://pay.example.com";

async function call(method, path, body) {
  const rawBody = body ? JSON.stringify(body) : "";
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomBytes(12).toString("hex");
  const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");

  const message = [method, path, timestamp, nonce, bodyHash].join("\n");
  const signature = crypto
    .createHmac("sha256", APP_SECRET)
    .update(message)
    .digest("hex");

  const response = await fetch(BASE + path, {
    method,
    headers: {
      "content-type": "application/json",
      "x-api-key": APP_KEY,
      "x-timestamp": timestamp,
      "x-nonce": nonce,
      "x-signature": signature,
    },
    body: body ? rawBody : undefined,
  });
  return response.json();
}

// 创建订单
const result = await call("POST", "/api/v1/orders", {
  outTradeNo: "ORDER_20261028_0001",
  amount: 1990,               // 单位:分
  subject: "星野咖啡 · 双人套餐",
  provider: "wechat",
  notifyUrl: "https://shop.example.com/pay/notify",
});
console.log(result.data.payUrl);

2. 创建订单

POST /api/v1/orders

字段类型必填说明
outTradeNostring是商户订单号,6-64 位,字母数字下划线短横线,需唯一(相同单号重复请求返回原订单)
amountint是金额,单位:分
subjectstring是商品标题,≤ 128 字
providerstring是alipay / wechat / unionpay / aggregator / mock
notifyUrlstring是支付结果通知地址(PayHub 回调你)
returnUrlstring否支付完成后的跳转地址
bodystring否商品描述
expiresInint否订单有效期(秒),默认 1800,范围 60-86400

响应:

{
  "success": true,
  "data": {
    "tradeNo": "PH20261028230001123456",
    "outTradeNo": "ORDER_20261028_0001",
    "amount": 1990,
    "amountText": "19.90",
    "provider": "wechat",
    "status": "PENDING",
    "payType": "qr",
    "payUrl": "https://pay.example.com/pay/PH2026...",
    "qrCode": "weixin://wxpay/bizpayurl?pr=xxxx",
    "expiresAt": "2026-10-28T15:30:01.000Z",
    "reused": false
  }
}

把 payUrl 渲染成二维码或跳转链接即可(本系统的收银台页面已经做了这件事)。

3. 查询订单

GET /api/v1/orders/<tradeNo>,返回订单状态、支付时间与退款记录。

4. 退款

POST /api/v1/refunds

字段类型必填说明
tradeNostring是平台订单号
amountint否退款金额(分),缺省为剩余可退金额
reasonstring否退款原因

5. 异步通知

订单支付成功后,PayHub 会向你的 notifyUrl 发送 POST JSON:

{
  "event": "payment.succeeded",
  "tradeNo": "PH20261028230001123456",
  "outTradeNo": "ORDER_20261028_0001",
  "provider": "wechat",
  "amount": 1990,
  "currency": "CNY",
  "subject": "星野咖啡 · 双人套餐",
  "status": "PAID",
  "paidAt": "2026-10-28T15:22:31.000Z",
  "merchantCode": "M00001",
  "sign": "...."
}
// 接收 PayHub 的支付结果通知
app.post("/pay/notify", (req, res) => {
  const { sign, ...rest } = req.body;
  const content = Object.keys(rest)
    .filter((key) => rest[key] !== undefined && rest[key] !== null && rest[key] !== "")
    .sort()
    .map((key) => `${key}=${rest[key]}`)
    .join("&");

  const expected = crypto
    .createHmac("sha256", process.env.PAYHUB_APP_SECRET)
    .update(content)
    .digest("hex");

  if (expected !== sign) return res.status(400).send("bad sign");
  if (orderAlreadyProcessed(rest.tradeNo)) return res.send("success"); // 幂等

  markOrderPaid(rest.outTradeNo, rest.amount);
  res.send("success");
});

6. 错误码

codeHTTP含义
MISSING_SIGNATURE401缺少签名请求头
TIMESTAMP_EXPIRED401时间戳超出允许偏差
INVALID_SIGNATURE401签名校验失败
DUPLICATE_NONCE409nonce 重复(疑似重放)
OUT_TRADE_NO_CONFLICT409商户订单号已存在但金额不一致
CHANNEL_UNAVAILABLE400商户未开通该支付通道
PROVIDER_ERROR502上游支付网关报错,详情见 message
AMOUNT_MISMATCH409上游回调金额与订单不一致(会被拒绝入账)
RATE_LIMITED429请求频率超限

7. 部署与运维