查询已有退款单并验证状态
读取自己已申请退款的状态,区分处理中、成功、关闭和异常。
← 返回平台功能接入准备
- 已完成中国大陆普通商户入驻、签约与对应支付产品配置,且查询自己的已有订单或退款单。
- 管理员在商户平台 账户中心 → API 安全 申请商户 API 证书并保管私钥,记录对应证书序列号。
- 本例使用微信支付公钥模式:在同一 API 安全页面申请并下载公钥,记录 PUB_KEY_ID;平台证书模式不能直接套用本例。
- 安装 Node.js 22,把密钥与公钥材料放在服务端受保护目录;不要上传到本网站或浏览器。
- 已由自己的业务系统或商户后台申请过退款,并保留 out_refund_no;本手册不会申请新退款。
准备环境变量
示例值需要替换为自己的凭证或资源 ID。代码在本机或服务端运行;密钥不应放进公开网页。
WX_MCH_ID- 普通商户号,在商户平台获取;不是 AppID。
WX_MCH_SERIAL- 商户 API 证书序列号,对应下方私钥;不是微信支付公钥 ID。
WX_PRIVATE_KEY_PATH- 自己服务器上商户 API 证书私钥 apiclient_key.pem 路径。
WX_PAY_PUBLIC_KEY_ID- API 安全页申请下载的微信支付公钥 ID,必须匹配当前验签公钥。
WX_PAY_PUBLIC_KEY_PATH- 同一 API 安全页下载的微信支付公钥文件路径,事先可信配置。
WX_OUT_REFUND_NO- 原申请退款时由自己商户系统生成的退款单号;不是微信 refund_id。
export WX_MCH_ID='1900000001'
export WX_MCH_SERIAL='REPLACE_WITH_MERCHANT_CERT_SERIAL'
export WX_PRIVATE_KEY_PATH='/secure/apiclient_key.pem'
export WX_PAY_PUBLIC_KEY_ID='PUB_KEY_ID_replace_me'
export WX_PAY_PUBLIC_KEY_PATH='/secure/wechatpay_public_key.pem'
export WX_OUT_REFUND_NO='DEMO_REFUND_20261015'以上 export 适用于 macOS / Linux 的终端,在同一终端内运行后续代码。Python 示例保存为 request.py 后执行 python3 request.py。
调用步骤
- 01
准备自己的商户号、商户 API 证书序列号及对应私钥,设置 WX_MCH_ID、WX_MCH_SERIAL、WX_PRIVATE_KEY_PATH。
- 02
在商户平台下载可信微信支付公钥,设置 WX_PAY_PUBLIC_KEY_ID 和 WX_PAY_PUBLIC_KEY_PATH;这两个参数负责验签,不参与商户请求签名。
- 03
设置该场景的商户订单号或退款单号;必须是自己业务后台创建时保存的值,不能用页面里的示意值作为真实资源。
- 04
把完整 JavaScript 保存为 request.mjs,使用 node request.mjs 运行。代码会生成当次 RSA 请求签名、验证原始响应再解析 JSON。
- 05
只有已验签的业务状态才能更新自己的订单;查询程序本身不会发起下单、退款、关单或付款。
查询单笔退款
通过 out_refund_no 查询,不在 URL 额外拼支付订单号或商户号;认证商户身份来自 Authorization。
https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/${WX_OUT_REFUND_NO}请求头
Acceptapplication/json
参数说明
参数表可左右滑动,查看完整说明。
| 字段 / 位置 | 类型 / 必填 | 说明 |
|---|---|---|
out_refund_noPath | string必填 | 原申请退款时的商户退款单号,最长 64 字符,必须属于当前认证商户。 |
AuthorizationHeader | string必填 | 商户私钥生成的 API v3 请求认证,详见完整 Node 代码。 |
调用代码
替换示例 ID 与请求体内容后运行。
import { readFileSync } from 'node:fs';
import { constants, randomBytes, sign, verify } from 'node:crypto';
const required = (name) => {
const value = process.env[name];
if (!value) throw new Error(`缺少环境变量 ${name}`);
return value;
};
const mchid = required('WX_MCH_ID');
const mchSerial = required('WX_MCH_SERIAL');
const payPublicKeyId = required('WX_PAY_PUBLIC_KEY_ID');
if (!/^\d+$/.test(mchid)) throw new Error('商户号应为数字字符串');
if (!/^[A-Fa-f0-9]+$/.test(mchSerial)) throw new Error('请填写真实商户 API 证书序列号');
if (!/^PUB_KEY_ID_\d+$/.test(payPublicKeyId)) throw new Error('本例仅支持已配置的微信支付公钥模式');
const privateKey = readFileSync(required('WX_PRIVATE_KEY_PATH'));
const payPublicKey = readFileSync(required('WX_PAY_PUBLIC_KEY_PATH'));
const outRefundNo = required('WX_OUT_REFUND_NO');
if (!/^[A-Za-z0-9_\-*|@]{1,64}$/.test(outRefundNo)) throw new Error('请填写自己的合法商户退款单号');
const url = new URL('https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/' + encodeURIComponent(outRefundNo));
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString('hex');
// 五行都以换行结束;GET 的最后一行是空 body,仍保留末尾换行。
const message = `GET\n${url.pathname}${url.search}\n${timestamp}\n${nonce}\n\n`;
const signature = sign('RSA-SHA256', Buffer.from(message), {
key: privateKey, padding: constants.RSA_PKCS1_PADDING,
}).toString('base64');
const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${mchid}",nonce_str="${nonce}",timestamp="${timestamp}",serial_no="${mchSerial}",signature="${signature}"`;
const response = await fetch(url, {
method: 'GET', redirect: 'error', signal: AbortSignal.timeout(20000),
headers: { Authorization: authorization, Accept: 'application/json',
'Wechatpay-Serial': payPublicKeyId, 'User-Agent': 'MerchantReadOnlyExample/1.0' },
});
const raw = Buffer.from(await response.arrayBuffer());
if (!response.ok) {
// 非 2xx 只作为错误提示,不将错误体作为可信业务结果。
throw new Error(`HTTP ${response.status}: ${raw.toString('utf8')}`);
}
const respSerial = response.headers.get('Wechatpay-Serial');
const respTimestamp = response.headers.get('Wechatpay-Timestamp') || '';
const respNonce = response.headers.get('Wechatpay-Nonce');
const respSignature = response.headers.get('Wechatpay-Signature');
if (respSerial !== payPublicKeyId) throw new Error('验签公钥 ID 不匹配,请从可信渠道核对轮换材料');
if (!/^\d+$/.test(respTimestamp) || !respNonce || !respSignature) throw new Error('响应缺少有效签名头');
if (Math.abs(Date.now() / 1000 - Number(respTimestamp)) > 300) throw new Error('响应时间偏差超过 5 分钟');
// 验签使用收到的原始字节;不能 JSON.parse 后重新序列化再验签。
const verificationMessage = Buffer.concat([
Buffer.from(`${respTimestamp}\n${respNonce}\n`), raw, Buffer.from('\n'),
]);
const verified = verify('RSA-SHA256', verificationMessage, {
key: payPublicKey, padding: constants.RSA_PKCS1_PADDING,
}, Buffer.from(respSignature, 'base64'));
if (!verified) throw new Error('响应验签失败,丢弃响应,不更新业务状态');
const data = JSON.parse(raw.toString('utf8'));
if (data.out_refund_no !== outRefundNo) throw new Error('返回的商户退款单号不匹配');
console.log(JSON.stringify({ out_refund_no: data.out_refund_no, refund_id: data.refund_id,
status: data.status, amount: data.amount }, null, 2));
// 正式业务须核对原支付单和退款金额,再幂等更新本地退款记录。
返回示例
按官方结构整理的节选示意,非实测结果。status 包括 SUCCESS、CLOSED、PROCESSING、ABNORMAL;HTTP 200 只代表查询完成。amount.refund、payer_refund 为分,业务应与本地退款记录核对。 以下为结构示意,非本站实测结果。
{
"refund_id": "50000000000000000000000000000",
"out_refund_no": "DEMO_REFUND_20261015",
"out_trade_no": "DEMO_ORDER_20261015",
"status": "PROCESSING",
"amount": {
"total": 100,
"refund": 100,
"payer_total": 100,
"payer_refund": 100,
"currency": "CNY"
}
}常见错误与排查
401 / SIGN_ERROR请求签名错误- 确认商户证书序列号与私钥匹配,GET 的空 body 仍保留结尾换行。
404 / RESOURCE_NOT_EXISTS退款单不存在- 核对 out_refund_no 是否属于该商户,以及原退款是否已被受理。
500 / SYSTEM_ERROR系统异常或超时- 稍后用同一查询单号与参数重试,不由查询失败触发新退款。
调用注意
- PROCESSING 不等于已退款到账;业务须结合可信退款通知或后续查单更新。
- 查询不发起退款,也不改变退款金额;不要因为查询超时而重新生成另一个退款单。
- 验签公钥只接受事先可信配置的文件与 ID,发生轮换时先核对新材料,不能信任响应里任意外部地址。
注意事项
- 这些是正式 API 的只读查询,必须有自己的商户和已有业务资源;本例不是通用免资金沙盒,也不创建测试支付。
- 本页无统一查单单价承诺;正式收款手续费、退款费用处理按经营类目和商户签约规则,查询示例不代表减免交易费用。
- 商户请求签名使用商户私钥,响应验签使用微信支付公钥;API v3 密钥用于其他需要对称解密的场景,不能替代任何一方。
核验来源
本站按官方资料整理本场景的必要调用步骤与代码,覆盖范围以本页列出的接口为准。官方资料用于追溯规范及查看变更。
- 01普通商户开发必要参数核验日期 2026-10-15
- 02带路径参数的 API v3 请求签名核验日期 2026-10-15
- 03微信支付公钥验签核验日期 2026-10-15
- 04普通商户查询单笔退款核验日期 2026-10-15
- 05验签前序列号与 5 分钟时间偏差检查核验日期 2026-10-15