支付宝开放平台调用手册
沙箱查询退款请求并验签
用原退款请求号读取退款结果,完整包含 RSA2 请求加签与原始同步响应验签。
← 返回平台功能接入准备
- 已在支付宝沙箱控制台创建或取得沙箱应用,确认该收单辅助接口在当前沙箱支持范围内。
- 沙箱应用采用普通公钥模式,已配置应用公钥,持有对应应用私钥及可信取得的沙箱支付宝公钥 PEM;证书模式不能直接使用本例。
- 安装 Node.js 22;准备自己的已有沙箱订单。没有订单时查询会返回交易不存在,不能当作代码签名失败。
- 已通过自己的沙箱联调流程发起过退款并保存 out_request_no,本页不会创建或重试退款。
准备环境变量
示例值需要替换为自己的凭证或资源 ID。代码在本机或服务端运行;密钥不应放进公开网页。
ALIPAY_APP_ID- 沙箱控制台中自己的 AppID,非正式应用 ID。
ALIPAY_PRIVATE_KEY_PATH- 自己生成并在沙箱配置对应公钥的应用私钥 PEM 文件,支持 Node 可读的 PKCS#1/PKCS#8。
ALIPAY_PUBLIC_KEY_PATH- 同一沙箱可信后台取得的支付宝公钥 PEM 文件;不是应用公钥。
ALIPAY_OUT_TRADE_NO- 自己先前已创建的沙箱商户订单号;本页不创建订单。
ALIPAY_OUT_REQUEST_NO- 原沙箱退款请求号;如原申请未传,使用当时的商户订单号。
export ALIPAY_APP_ID='2016091500000000'
export ALIPAY_PRIVATE_KEY_PATH='/secure/sandbox-app-private.pem'
export ALIPAY_PUBLIC_KEY_PATH='/secure/sandbox-alipay-public.pem'
export ALIPAY_OUT_TRADE_NO='SANDBOX_ORDER_20261015'
export ALIPAY_OUT_REQUEST_NO='SANDBOX_REFUND_20261015'以上 export 适用于 macOS / Linux 的终端。按调用步骤保存代码文件,再在同一终端执行。
调用步骤
- 01
在同一终端设置下方沙箱环境变量,三项应用与密钥材料必须来自同一套沙箱配置。
- 02
把完整 JavaScript 保存为 request.mjs,用 node request.mjs 运行。代码网关固定沙箱,只发起一个只读查询。
- 03
先验签,再检查业务 code 和订单号;核对 refund_status=REFUND_SUCCESS 及金额,不能只看 code=10000。
调用 01
查询已有沙箱退款
这是 v2 gateway.do 表单接口,method 和 biz_content 是表单字段。下方请求体是字段结构示意;完整代码负责序列化 biz_content、生成当次签名并验证原始响应。
POST
https://openapi-sandbox.dl.alipaydev.com/gateway.do请求头
Content-Typeapplication/x-www-form-urlencoded;charset=utf-8
参数说明
参数表可左右滑动,查看完整说明。
| 字段 / 位置 | 类型 / 必填 | 说明 |
|---|---|---|
app_id请求体 | string必填 | 沙箱应用 AppID。 |
method请求体 | string必填 | alipay.trade.fastpay.refund.query,固定接口名称。 |
charset / format / version请求体 | string必填 | 本例 utf-8、JSON、1.0。 |
timestamp请求体 | string必填 | 北京时间 yyyy-MM-dd HH:mm:ss。 |
sign_type / sign请求体 | string必填 | RSA2;除 sign 和空值外参数按 ASCII 键名排序后,用应用私钥 RSA-SHA256 加签。 |
biz_content.out_trade_no请求体 | string必填 | 本例按商户订单号查询,最长64字符;原 API 也允许 trade_no 二选一,同传时 trade_no 优先。 |
biz_content.out_request_no请求体 | string必填 | 原退款请求号,最长64字符;不能为查询新生成另一个退款号。 |
调用代码
替换示例 ID 与请求体内容后运行。
import { readFileSync } from 'node:fs';
import { constants, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';
const required = (name) => {
const value = process.env[name];
if (!value) throw new Error(`缺少环境变量 ${name}`);
return value;
};
const appId = required('ALIPAY_APP_ID');
if (!/^\d{16}$/.test(appId)) throw new Error('请填写自己的沙箱 AppID');
const privateKey = createPrivateKey(readFileSync(required('ALIPAY_PRIVATE_KEY_PATH')));
const alipayKey = createPublicKey(readFileSync(required('ALIPAY_PUBLIC_KEY_PATH')));
if (privateKey.asymmetricKeyType !== 'rsa' || privateKey.asymmetricKeyDetails.modulusLength < 2048) {
throw new Error('本例需要 RSA 2048 位或更长的应用私钥');
}
// 网关固定为官方当前沙箱;本例不允许配置正式网关。
const gateway = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do';
const outTradeNo = required('ALIPAY_OUT_TRADE_NO');
if (!outTradeNo || outTradeNo.length > 64) throw new Error('商户订单号最长 64 字符');
const outRequestNo = required('ALIPAY_OUT_REQUEST_NO');
if (!outRequestNo || outRequestNo.length > 64) throw new Error('退款请求号最长 64 字符');
const bizContent = { out_trade_no: outTradeNo, out_request_no: outRequestNo };
// 生成北京时间 yyyy-MM-dd HH:mm:ss;机器时钟应保持同步。
const parts = Object.fromEntries(new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
}).formatToParts(new Date()).map((part) => [part.type, part.value]));
const timestamp = `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`;
const parameters = { app_id: appId, method: 'alipay.trade.fastpay.refund.query', format: 'JSON',
charset: 'utf-8', sign_type: 'RSA2', timestamp, version: '1.0',
biz_content: JSON.stringify(bizContent) };
// 先按键名 ASCII 顺序拼接原始值;签名后再做表单 URL 编码。
const signingText = Object.keys(parameters).filter((key) => parameters[key] !== '')
.sort().map((key) => `${key}=${parameters[key]}`).join('&');
parameters.sign = sign('RSA-SHA256', Buffer.from(signingText, 'utf8'), {
key: privateKey, padding: constants.RSA_PKCS1_PADDING,
}).toString('base64');
const response = await fetch(gateway, { method: 'POST', redirect: 'error',
signal: AbortSignal.timeout(20000),
headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' },
body: new URLSearchParams(parameters).toString() });
const raw = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const envelope = JSON.parse(raw);
const responseKey = 'alipay_trade_fastpay_refund_query_response';
if (!envelope[responseKey] || typeof envelope.sign !== 'string') {
throw new Error('响应缺少预期业务节点或签名,不能作为可信业务结果');
}
// 只提取响应节点的原始 JSON 字符串,不对业务对象重新序列化。
// 扫描时处理 JSON 字符串内的引号与转义,避免正文中的大括号截断节点。
const marker = new RegExp('"' + responseKey + '"\\s*:\\s*').exec(raw);
if (!marker) throw new Error('找不到原始响应节点');
const start = marker.index + marker[0].length;
if (raw[start] !== '{') throw new Error('业务响应节点不是对象');
let depth = 0, inString = false, escaped = false, end = -1;
for (let i = start; i < raw.length; i++) {
const char = raw[i];
if (inString) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === '"') inString = false;
continue;
}
if (char === '"') inString = true;
else if (char === '{' || char === '[') depth++;
else if (char === '}' || char === ']') {
depth--;
if (depth === 0) { end = i + 1; break; }
}
}
if (end < 0) throw new Error('业务响应 JSON 不完整');
const signedText = raw.slice(start, end);
if (!verify('RSA-SHA256', Buffer.from(signedText, 'utf8'), {
key: alipayKey, padding: constants.RSA_PKCS1_PADDING,
}, Buffer.from(envelope.sign, 'base64'))) {
throw new Error('支付宝响应验签失败,不更新业务记录');
}
// 业务数据仅来自已验签的原始节点,避免重排、转义或未验证字段影响结果。
const result = JSON.parse(signedText);
if (result.code !== '10000') {
throw new Error(`${result.code}: ${result.sub_code || ''} ${result.sub_msg || result.msg || ''}`);
}
if (result.out_trade_no !== outTradeNo) throw new Error('返回订单号与查询订单不匹配');
if (result.out_request_no !== outRequestNo) throw new Error('返回退款请求号不匹配');
console.log(JSON.stringify({ out_trade_no: result.out_trade_no, out_request_no: result.out_request_no,
refund_status: result.refund_status || 'NOT_CONFIRMED', refund_amount: result.refund_amount }, null, 2));
// 正式业务还必须核对商户、应用、金额和币种,幂等处理;本例只输出沙箱查询结果。
返回示例
已签名响应的结构节选示意;sign 为外层同步响应签名。金额单位为元。本次查询成功与业务成功分开判断。缺少 refund_status 时不能确认退款成功。 以下为结构示意,非本站实测结果。
{
"alipay_trade_fastpay_refund_query_response": {
"code": "10000",
"msg": "Success",
"out_trade_no": "SANDBOX_ORDER_20261015",
"trade_no": "2026101522000000000000000000",
"out_request_no": "SANDBOX_REFUND_20261015",
"refund_status": "REFUND_SUCCESS",
"refund_amount": "1.00",
"total_amount": "1.00"
},
"sign": "BASE64_RESPONSE_SIGNATURE_EXAMPLE"
}常见错误与排查
isv.invalid-signature请求签名无效- 核对同套沙箱应用密钥,sign_type 参与签名,先拼原始参数值再 URL 编码;不要拿应用公钥验支付宝响应。
ACQ.TRADE_NOT_EXIST查询交易不存在- 确认订单号来自相同沙箱应用和商户;本代码不会创建测试订单。
ACQ.INVALID_PARAMETER业务参数无效- 按官方字段核对订单号与原退款请求号。
调用注意
- 请求签名使用应用私钥,响应验签使用可信的支付宝公钥。代码验证原始响应业务节点,不对 JSON 对象重新序列化后验签。
- 示例只有 Node.js 22,使用内置 crypto 和 fetch,不提供缺少签名的 cURL。
- 沙箱与正式环境不能混用。本页不会执行下单、退款、转账或关单,也没有在线执行请求功能。
- 官方建议退款申请后间隔10秒以上查询;未确认成功时不能新建另一个退款号重试。
注意事项
- 仅适用于 OpenAPI v2 的 RSA2 普通公钥模式;不适用于 OpenAPI v3 或证书模式。
- 沙箱使用模拟数据且无真实交易费用,不代表正式商户资格或资源开销已免除。
- 未调用真实账号或沙箱 API,未声明签名被平台接受或资金状态已验证。
核验来源
本站按官方资料整理本场景的必要调用步骤与代码,覆盖范围以本页列出的接口为准。官方资料用于追溯规范及查看变更。
- 01沙箱密钥、网关与模拟费用说明核验日期 2026-10-15
- 02开放平台创建应用与沙箱接入核验日期 2026-10-15
- 03统一收单交易退款查询 API核验日期 2026-10-15
- 04RSA2 参数签名规则核验日期 2026-10-15
- 05公钥模式签名与验签配置核验日期 2026-10-15
- 06支付宝官方 Node SDK:原始同步响应验签核验日期 2026-10-15
- 07支付宝官方 Node SDK:RSA2 算法映射与加签核验日期 2026-10-15
