异步通知

支付回调 / 退款回调 / 审核回调

SMP 主动向接入方 POST 三类异步通知:支付成功退款结果小程序审核结果。 通知 body 用调用方 credential 的 sha256(apiSecret) 做 HMAC 签名,与入向请求算法一致。

通用 Header

http
POST <你注册的 notify URL>
Content-Type: application/json
X-Api-Key:       <你的 apiKey>
X-Timestamp:     <ms>
X-Service-Code:  payments | miniprogram
X-Signature:     <hmac-sha256 hex>

验签

ts
// 复用 quickstart 的 sign() 工具
function verify(req) {
  const ts = req.headers["x-timestamp"];
  const sig = req.headers["x-signature"];
  const serviceCode = req.headers["x-service-code"];

  if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return false;

  const expected = sign(SMP_API_SECRET, serviceCode, req.body).sig;
  return expected === sig;
}

body 是已被 JSON.parse 后的对象,sign() 内部会重新 stringify 一次 — Node 默认 JSON 序列化对 plain object 的 round-trip 是稳定的。


1. 支付成功 / 退款

注册位置:每笔订单创建时的 notifyUrl(支付和退款共用同一个 URL)。

支付成功

json
{
  "outTradeNo": "your_unique_id",
  "orderId": "<smp 订单 id>",
  "platformId": "<your platform id>",
  "channel": "wechat",
  "amount": 99,
  "status": "paid",
  "transactionId": "4200001234...",
  "paidAt": "2026-05-07T12:00:00Z",
  "metadata": { "...": "..." }
}

退款结果

json
{
  "outTradeNo": "your_unique_id",
  "orderId": "<smp 订单 id>",
  "platformId": "<your platform id>",
  "channel": "wechat",
  "amount": 99,
  "status": "refunded",
  "transactionId": "...",
  "metadata": {
    "notify_event": "refund",
    "last_refund": {
      "out_refund_no": "...",
      "refund_amount": 99,
      "reason": "...",
      "refunded_at": "..."
    }
  }
}

⚠️ 退款回调和支付回调是同一个 URL,业务方靠statusmetadata.notify_event 区分。

幂等

SMP 转发失败只 log,不重试,但微信侧会按自己的策略反复发,所以接收端必须幂等:建议按(outTradeNo, status, transactionId) 去重。

响应

返回任意 2xx 即可。SMP 不要求特定 body —— 失败时不会重试,请保证接收端业务处理本身的可靠性(数据库事务 / 队列)。


2. 小程序审核结果

注册位置:在微盈云 admin 后台 platform 配置里设config.miniprogram.audit_notify_url(不设则不推送,要主动 polling)。

json
{
  "platformId": "<your platform id>",
  "platform": "wechat",            // 或 alipay
  "authorizerAppId": "wx...",
  "auditId": "...",
  "status": "approved",            // 或 rejected
  "feedback": "<rejected 时附原因>",
  "occurredAt": "2026-05-07T12:00:00Z",
  "raw": { "...": "原始平台 callback 字段" }
}

完整接收端示例

ts
import express from "express";
import crypto from "crypto";

const SMP_API_SECRET = process.env.SMP_API_SECRET!;
const sign = (sc: string, body: any) => {
  const key = crypto.createHash("sha256").update(SMP_API_SECRET).digest("hex");
  const ts = Date.now().toString();
  const payload = ts + sc.toLowerCase() + JSON.stringify(body ?? {});
  return crypto.createHmac("sha256", key).update(payload).digest("hex");
};

const app = express();
app.use(express.json());

app.post("/notify", async (req, res) => {
  const ts = String(req.headers["x-timestamp"] ?? "");
  const sig = String(req.headers["x-signature"] ?? "");
  const sc = String(req.headers["x-service-code"] ?? "payments");

  if (!ts || Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) {
    return res.status(401).end("expired");
  }
  if (sign(sc, req.body) !== sig) {
    return res.status(401).end("invalid sig");
  }

  const { outTradeNo, status, metadata, transactionId } = req.body;
  const isRefund = metadata?.notify_event === "refund" || status === "refunded";

  // 幂等:先检查已处理过这个 outTradeNo+status 没有
  if (await alreadyHandled(outTradeNo, status)) {
    return res.status(204).end();
  }

  if (isRefund) {
    await markRefunded(outTradeNo);
  } else if (status === "paid") {
    await fulfill(outTradeNo, transactionId);
  }

  res.status(204).end();
});