异步通知
支付回调 / 退款回调 / 审核回调
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,业务方靠status 或 metadata.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();
});