GLODIPAY 退款 API 规范
版本 2.0.0
目录
- 介绍
- 接口地址 (Endpoints)
- 签名 (Signature)
- 退款 API (REFUND API)
- 退款查询 (REFUND QUERY)
- 退款通知 (REFUND NOTIFICATION)
- 附录
- 退款状态值
- 状态码
- 代码示例
- PHP
- Node.js
介绍
本文档描述了 GLODIPAY 退款 API v2,它允许商户为已完成的交易发起退款、查询退款状态并通过 IPN 接收退款通知。
关键点:
- 退款可以是全额或部分(取决于各渠道的限制 —— 某些渠道仅支持全额退款)。
- 根据商户配置,退款可能会被立即自动处理或保留以待人工审核。
- 原始交易的
notificationUrl用于接收退款 IPN 通知。 - 每次退款的最大可退金额为:
paidAmount减去该交易已退款的总额。
接口地址 (Endpoints)
| 测试环境 | 将另行提供 |
| 生产环境 | 从商户后台的 API Keys 页面获取 |
签名 (Signature)
所有发送给 GLODIPAY 的请求必须包含 signature 字段。所有来自 GLODIPAY 的响应和 IPN Payload 也包含 signature 字段以验证真实性。
签名使用 RSA with MD5 (md5WithRSAEncryption)。
生成签名 (商户 -> GLODIPAY)
使用您的 商户私钥(可从商户后台获取)。
步骤:
- 将除
signature以外的所有请求参数收集为扁平的键值对象。 - 仅按 自然升序 对 顶层键 进行排序(PHP 中的
SORT_NATURAL/ JavaScript 中带有{ numeric: true }的localeCompare)。不要对嵌套对象内部的键进行排序。 - 递归修整所有字符串值的空格。
- 序列化为 JSON 字符串。
- 使用
openssl_sign(..., 'md5WithRSAEncryption')进行签名。 - 对二进制输出进行 Base64 编码。
验证签名 (GLODIPAY -> 商户)
使用 GLODIPAY 公钥(可在门户网站获取)。
步骤:
- 从 Payload 中移除
signature。 - 仅按自然升序对 顶层键 进行排序。不要对嵌套对象内部的键进行排序。
- 递归修整所有字符串值的空格。
- 序列化为 JSON 字符串。
- 使用
openssl_verify(..., base64_decode($signature), $publicKey, 'md5WithRSAEncryption')进行验证。 - 返回值
1= 有效。
注意 (Node.js):在排序/序列化之前将所有数字值转换为字符串。转义 JSON 字符串中的正斜杠:.replace(///g, '\/')。
退款 API (REFUND API)
为已完成的交易发起退款。
接口地址: POST /v2/refund
请求方法: POST
Content-Type: application/json
请求 (Request)
| transactionId | String (ULID) | M | 从结账 IPN 或交易查询接收到的 transactionId |
| amount | Float | M | 退款金额。最小值:0.10。最大值:剩余可退款金额(paidAmount 减去已退款金额) |
| reason | String(max:1000) | O | 退款原因的简短描述 |
| signature | String(max:750) | M | RSA-MD5 签名 |
注意:某些渠道(如 PayAgency, SmartPay, ClisaPay, FinvyPay, WPay)仅支持全额退款。系统将相应地强制执行最小退款金额。
响应 (Response)
Content-Type: application/json
| status | String | success |
| message | String | 人类可读的消息 |
| data | JSON | 退款详情。请参阅下面的 data 对象 |
data 对象:
| refundId | String (ULID) | GLODIPAY 退款 ID |
| refundNumber | String | GLODIPAY 人类可读的退款编号 |
退款已创建并立即处理(启用了自动退款):
{
"status": "success",
"message": null,
"data": {
"refundId": "01jzabk09xc4pbgwe8hyg4cwbf",
"refundNumber": "2507-1751420414"
}
}
退款已创建并等待人工审核:
{
"status": "success",
"message": "Refund created and waiting for approval.",
"data": {
"refundId": "01jzabk09xc4pbgwe8hyg4cwbf",
"refundNumber": "2507-1751420414"
}
}
验证错误 (HTTP 422):
{
"status": "error",
"message": "Invalid request data.",
"errors": [
{
"field": "amount",
"message": ["The amount must be between 0.1 and 100."]
}
]
}
退款查询 (REFUND QUERY)
查询退款的最新状态。
接口地址: POST /v2/refund/query
请求方法: POST
Content-Type: application/json
请求 (Request)
| refundId | String (ULID) | M | GLODIPAY 退款 ID(来自退款 API 响应或退款 IPN) |
| signature | String(max:750) | M | RSA-MD5 签名 |
响应 (Response)
Content-Type: application/json
| status | String | success |
| message | String | 人类可读的消息 |
| data | JSON | 退款详情对象。请参阅下面的字段 |
data 对象字段:
| transactionId | String | GLODIPAY 原 始交易 ID |
| ref | String | 商户的 orderRef |
| refundId | String | GLODIPAY 退款 ID |
| currency | String | ISO 4217 货币代码 |
| refundAmount | Float | 退款金额 |
| status | String | 退款状态。请参阅“退款状态值”部分 |
| statusCode | Number | 数字状态码。请参阅“状态码”部分 |
| metadata | JSON | 原始结账会话中的键值对 |
| reason | String | 退款原因 |
| message | String | 人类可读的状态消息 |
| originalRefundCreatedAt | ISO 8601 datetime | 支付渠道侧的退款创建时间 |
| refundCreatedAt | ISO 8601 datetime | GLODIPAY 系统中的退款创建时间 |
| transactionCreatedAt | ISO 8601 datetime | 原始交易创建时间 |
| signature | String | RSA-MD5 签名 —— 使用 GLODIPAY 公钥验证 |
示例:
{
"status": "success",
"message": "",
"data": {
"transactionId": "01jza90dy6w82dfrrqvadn5vs4",
"ref": "ORDER-001",
"refundId": "01jzabk09xc4pbgwe8hyg4cwbf",
"currency": "USD",
"refundAmount": 50.00,
"status": "refund_successful",
"statusCode": 11,
"metadata": { "orderId": "12345" },
"reason": "Customer request",
"message": null,
"originalRefundCreatedAt": "2026-04-14T11:00:00+00:00",
"refundCreatedAt": "2026-04-14T11:00:01+00:00",
"transactionCreatedAt": "2026-04-14T10:00:00+00:00",
"signature": "base64-encoded-rsa-signature"
}
}
退款通知 (REFUND NOTIFICATION)
当退款状态发生变化时,GLODIPAY 会向原始交易的 notificationUrl 发送 HTTP POST 请求。
请求方法: POST
Content-Type: application/json
重试策略: 如果您的服务器未能在 30 秒 内返回 {"returnCode":"100"},GLODIPAY 将重试发送。
Payload
| transactionId | String | M | GLODIPAY 原始交易 ID |
| ref | String | M | 商户的 orderRef |
| refundId | String | M | GLODIPAY 退款 ID |
| currency | String | M | ISO 4217 货币代码 |
| refundAmount | Float | M | 退款金额 |
| status | String | M | 退款状态。请参阅“退款状态值”部分 |
| statusCode | Number | M | 数字状态码。请参阅“状态码”部分 |
| metadata | JSON | O | 原始结账会话中的键值对 |
| reason | String | O | 退款原因 |
| message | String | O | 人类可读的状态消息 |
| originalRefundCreatedAt | ISO 8601 datetime | M | 支付渠道侧的退款创建时间 |
| refundCreatedAt | ISO 8601 datetime | M | GLODIPAY 系统中的退款创建时间 |
| transactionCreatedAt | ISO 8601 datetime | M | 原始交易创建时间 |
| signature | String | M | RSA-MD5 签名 —— 使用 GLODIPAY 公钥验证 |
IPN Payload 示例:
{
"transactionId": "01jza90dy6w82dfrrqvadn5vs4",
"ref": "ORDER-001",
"refundId": "01jzabk09xc4pbgwe8hyg4cwbf",
"currency": "USD",
"refundAmount": 50.00,
"status": "refund_successful",
"statusCode": 11,
"metadata": { "orderId": "12345" },
"reason": "Customer request",
"message": null,
"originalRefundCreatedAt": "2026-04-14T11:00:00+00:00",
"refundCreatedAt": "2026-04-14T11:00:01+00:00",
"transactionCreatedAt": "2026-04-14T10:00:00+00:00",
"signature": "base64-encoded-rsa-signature"
}
响应 (商户 -> GLODIPAY)
您的服务器必须在 30 秒 内响应:
{
"returnCode": "100",
"description": "Received"
}
| returnCode | String | R | 必须为 "100" 以确认收到 |
| description | String(1,1500) | O | 可选描述 |
附录
退款状态值
在退款 IPN Payload 和退款查询响应的 status 字段中返回的字符串值。
| refund_initiated | 退款请求已发起 |
| refund_under_review | 退款审核中 |
| refund_successful | 退款成功完成 |
| refund_failed | 退款失败 |
| refund_partially_successful | 部分退款完成 |
| refund_partially_failed | 部分退款失败 |
| void_initiated | 撤销已发起 |
| void_under_review | 撤销审核中 |
| void_successful | 撤销成功完成 |
| void_failed | 撤销失败 |
| void_partially_successful | 部分撤销完成 |
| void_partially_failed | 部分撤销失败 |
状态码
在退款 IPN Payload 和退款查询响应的 statusCode 字段中的数字代码。
| 8 | refund_initiated | 退款已发起 |
| 9 | refund_failed | 退款失败 |
| 10 | refund_under_review | 退款审核中 |
| 11 | refund_successful | 退款成功 |
| 12 | refund_partially_failed | 部分退款失败 |
| 13 | refund_partially_successful | 部分退款成功 |
| 18 | void_initiated | 撤销已发起 |
| 19 | void_under_review | 撤销审核中 |
| 20 | void_successful | 撤销成功 |
| 21 | void_failed | 撤销失败 |
| 22 | void_partially_successful | 部分撤销成功 |
| 23 | void_partially_failed | 部分撤销失败 |
代码示例
PHP
<?php
function generateSignature(array $data): string
{
$privateKey = openssl_pkey_get_private("-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY_HERE
-----END PRIVATE KEY-----
");
ksort($data, SORT_NATURAL);
array_walk_recursive(
$data,
static function (&$field) {
$field = trim($field);
}
);
openssl_sign(json_encode($data), $signature, $privateKey, 'md5WithRSAEncryption');
return base64_encode($signature);
}
function verifySignature(array $data): bool
{
$publicKey = openssl_pkey_get_public("-----BEGIN PUBLIC KEY-----
YOUR_GLODIPAY_PUBLIC_KEY_HERE
-----END PUBLIC KEY-----
");
$dataWithoutSignature = array_filter($data, static function ($key) {
return $key !== 'signature';
}, ARRAY_FILTER_USE_KEY);
$signature = $data['signature'];
ksort($dataWithoutSignature, SORT_NATURAL);
array_walk_recursive(
$dataWithoutSignature,
static function (&$field) {
$field = trim($field);
}
);
$result = openssl_verify(
json_encode($dataWithoutSignature),
base64_decode($signature),
$publicKey,
'md5WithRSAEncryption'
);
return $result === 1;
}
// 示例:发起退款
$payload = [
'transactionId' => '01jza90dy6w82dfrrqvadn5vs4',
'amount' => '50.00',
'reason' => 'Customer request',
];
$payload['signature'] = generateSignature($payload);
$ch = curl_init('https://payment-sandbox.gpayprocessing.com/v2/refund');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
echo 'Refund ID: ' . $result['data']['refundId'];
Node.js
// Save as script.mjs and run: node script.mjs
import { createSign } from 'crypto';
import https from 'https';
const PRIVATE_KEY = `-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY_HERE
-----END PRIVATE KEY-----`;
function phpCast(v) {
if (typeof v === 'number') return String(v);
if (typeof v === 'boolean') return v ? '1' : '';
if (typeof v === 'string') return v.trim();
if (Array.isArray(v)) return v.map(phpCast);
if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, val]) => [k, phpCast(val)]));
return v;
}
function generateSignature(data) {
const sorted = {};
Object.keys(data)
.filter(k => k !== 'signature')
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }))
.forEach(k => { sorted[k] = data[k]; });
const canonical = JSON.stringify(phpCast(sorted))
.replace(/\//g, '\\/')
.replace(/[\u0080-\uffff]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
const sign = createSign('md5WithRSAEncryption');
sign.update(canonical);
return sign.sign(PRIVATE_KEY, 'base64');
}
// 示例:发起退款
const payload = {
transactionId: '01jza90dy6w82dfrrqvadn5vs4',
amount: '50.00',
reason: 'Customer request',
};
payload.signature = generateSignature(payload);
const postData = JSON.stringify(payload);
const options = {
hostname: 'payment-sandbox.gpayprocessing.com',
path: '/v2/refund',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
},
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
const result = JSON.parse(body);
console.log('Refund ID:', result.data.refundId);
});
});
req.on('error', console.error);
req.write(postData);
req.end();