退款
Refund Service 提供退款的创建、查询、取消等核心功能。
端点
| 方法 | 端点 | 描述 |
|---|---|---|
| POST | /api/v1/refunds/create | 创建退款 |
| GET | /api/v1/refunds/:refund_id | 获取退款信息 |
| POST | /api/v1/refunds/:refund_id/cancel | 取消退款 |
概述
认证方式
所有API请求都需要在HTTP请求头中包含以下认证信息:
必需请求头
| 请求头 | 类型 | 必填 | 说明 |
|---|---|---|---|
| Content-Type | string | 是 | 固定为 application/json,GET请求不必携带 |
| Authorization | string | 是 | API密钥,格式:Bearer {api_key} |
| X-StablePay-Timestamp | string | 是 | 请求时间戳(Unix时间戳,秒级) |
| X-StablePay-Nonce | string | 是 | 随机数(用于防重放攻击) |
| X-StablePay-Signature | string | 是 | 请求签名(HMAC-SHA256) |
注意:
- 商户ID和API密钥由StablePay平台分配,请妥善保管,不要泄露
- API密钥格式:
Bearer sk_live_xxx(生产环境)或Bearer sk_test_xxx(测试环境)
请求签名
为了确保请求的安全性,所有API请求都需要包含签名信息。签名使用HMAC-SHA256算法计算。
签名计算步骤
准备签名参数:
timestamp: 当前Unix时间戳(秒级)nonce: 随机字符串(建议使用UUID)requestBody: 请求体的JSON字符串(GET请求为空字符串)
构建签名字符串:
signString = timestamp + "." + nonce + "." + requestBody计算签名:
signature = HMAC-SHA256(api_secret, signString)其中
api_secret是API密钥对应的密钥(从API密钥中提取或单独提供)设置请求头:
X-StablePay-Timestamp: {timestamp} X-StablePay-Nonce: {nonce} X-StablePay-Signature: {signature}
签名示例代码
JavaScript/Node.js:
const crypto = require('crypto');
function calculateSignature(apiSecret, timestamp, nonce, requestBody) {
const signString = `${timestamp}.${nonce}.${requestBody}`;
const signature = crypto
.createHmac('sha256', apiSecret)
.update(signString)
.digest('hex');
return signature;
}
// 使用示例 / Usage example
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = require('uuid').v4();
const requestBody = JSON.stringify({
order_id: 'sess_xxxxxxxxxxxx',
refund_id: 'refund_20250101001',
amount: '50.00',
currency: 'USDT'
});
const signature = calculateSignature(apiSecret, timestamp, nonce, requestBody);
// 设置请求头 / Set request headers
headers['X-StablePay-Timestamp'] = timestamp;
headers['X-StablePay-Nonce'] = nonce;
headers['X-StablePay-Signature'] = signature;
Python:
import hmac
import hashlib
import time
import uuid
import json
def calculate_signature(api_secret, timestamp, nonce, request_body):
"""计算请求签名 / Calculate request signature"""
sign_string = f"{timestamp}.{nonce}.{request_body}"
signature = hmac.new(
api_secret.encode('utf-8'),
sign_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature
# 使用示例 / Usage example
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
request_body = json.dumps({
'order_id': 'sess_xxxxxxxxxxxx',
'refund_id': 'refund_20250101001',
'amount': '50.00',
'currency': 'USDT'
})
signature = calculate_signature(api_secret, timestamp, nonce, request_body)
# 设置请求头 / Set request headers
headers['X-StablePay-Timestamp'] = timestamp
headers['X-StablePay-Nonce'] = nonce
headers['X-StablePay-Signature'] = signature
Go:
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/google/uuid"
)
func calculateSignature(apiSecret, timestamp, nonce, requestBody string) string {
signString := fmt.Sprintf("%s.%s.%s", timestamp, nonce, requestBody)
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(signString))
return hex.EncodeToString(mac.Sum(nil))
}
// 使用示例 / Usage example
timestamp := fmt.Sprintf("%d", time.Now().Unix())
nonce := uuid.New().String()
requestBody := `{"order_id":"sess_xxxxxxxxxxxx","refund_id":"refund_20250101001","amount":"50.00","currency":"USDT"}`
signature := calculateSignature(apiSecret, timestamp, nonce, requestBody)
// 设置请求头 / Set request headers
headers["X-StablePay-Timestamp"] = timestamp
headers["X-StablePay-Nonce"] = nonce
headers["X-StablePay-Signature"] = signature
签名验证说明
- 服务端会验证时间戳是否在允许的范围内(通常为5分钟)
- 服务端会验证nonce是否已被使用(防重放攻击)
- 服务端会重新计算签名并与请求头中的签名进行比对
接口列表
1. 创建退款
为指定的支付会话创建退款请求。
重要说明:
order_id参数必须是支付会话ID,格式为sess_xxxxxxxxxxxx- 支付会话ID可以通过创建支付会话接口返回的
id字段获取 - 只有已完成(
paid)的支付会话才能申请退款
接口地址
POST /api/v1/refunds/create
请求头
Content-Type: application/json
Authorization: Bearer sk_live_xxxxxxxxxx
X-StablePay-Timestamp: 1735123456
X-StablePay-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-StablePay-Signature: 3c8f9d2a7b6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| order_id | string | 是 | 要退款的订单ID(格式:sess_xxxxxxxxxxxx,即支付会话ID) |
| refund_id | string | 是 | 退款ID(商户侧唯一,用于幂等性控制) |
| amount | string | 是 | 退款金额(面额字符串,如 "10.50") |
| currency | string | 是 | 货币类型:USDT 或 USDC |
| reason | string | 否 | 退款原因 |
| description | string | 否 | 退款描述 |
| metadata | object | 否 | 元数据(键值对) |
退款原因枚举
| 值 | 说明 |
|---|---|
requested_by_customer | 客户要求退款 |
duplicate | 重复支付 |
fraudulent | 欺诈交易 |
expired_uncaptured | 过期未捕获 |
请求示例
{
"order_id": "sess_xxxxxxxxxxxx",
"refund_id": "refund_20250101001",
"amount": "50.00",
"currency": "USDT",
"reason": "requested_by_customer",
"description": "客户申请退款",
"metadata": {
"customer_id": "customer_123",
"refund_note": "商品质量问题"
}
}
重要说明:
order_id必须是支付会话ID,格式为sess_xxxxxxxxxxxx(即创建支付会话时返回的id字段)- 只有已完成的支付会话才能申请退款
响应示例
{
"refund_id": "refund_20250101001",
"session_id": "cs_xxxxxxxxxxxx",
"order_id": "order_20250101001",
"amount": "50.00",
"currency": "USDT",
"status": "pending",
"reason": "requested_by_customer",
"description": "客户申请退款",
"created_at": 1735123456,
"metadata": {
"customer_id": "customer_123",
"refund_note": "商品质量问题"
}
}
响应字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| refund_id | string | 退款ID(商户提供的退款ID) |
| session_id | string | 支付会话ID |
| order_id | string | 订单ID |
| amount | string | 退款金额(面额字符串) |
| currency | string | 货币类型 |
| status | string | 退款状态 |
| reason | string | 退款原因 |
| description | string | 退款描述 |
| created_at | number | 创建时间戳(Unix时间戳) |
| metadata | object | 元数据 |
状态码
201 Created: 退款创建成功400 Bad Request: 请求参数错误401 Unauthorized: 认证失败403 Forbidden: 无权访问该支付会话404 Not Found: 支付会话不存在429 Too Many Requests: 超过每日退款限制
2. 获取退款信息
根据退款ID获取退款详情。
接口地址
GET /api/v1/refunds/{refund_id}
请求头
Authorization: Bearer sk_live_xxxxxxxxxx
X-StablePay-Timestamp: 1735123456
X-StablePay-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-StablePay-Signature: 3c8f9d2a7b6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| refund_id | string | 是 | 退款ID |
请求示例
curl -X GET "https://api.stablepay.co/api/v1/refunds/refund_20250101001" \
-H "Authorization: Bearer sk_live_xxxxxxxxxx" \
-H "X-StablePay-Timestamp: 1735123456" \
-H "X-StablePay-Nonce: 550e8400-e29b-41d4-a716-446655440000" \
-H "X-StablePay-Signature: 3c8f9d2a7b6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0"
响应示例
{
"refund_id": "refund_20250101001",
"session_id": "cs_xxxxxxxxxxxx",
"order_id": "order_20250101001",
"amount": "50.00",
"currency": "USDT",
"status": "completed",
"reason": "requested_by_customer",
"description": "客户申请退款",
"failure_reason": null,
"transaction_hash": "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
"receipt_number": "RCPT_xxxxxxxxxxxx",
"created_at": 1735123456,
"processed_at": 1735123600,
"metadata": {
"customer_id": "customer_123",
"refund_note": "商品质量问题"
}
}
状态码
200 OK: 查询成功400 Bad Request: 请求参数错误401 Unauthorized: 认证失败403 Forbidden: 无权访问该退款404 Not Found: 退款不存在
3. 取消退款
取消正在处理中的退款请求。
注意事项:
- 只有状态为
processing(处理中)的退款才能取消 - 已完成的退款(
completed、failed、canceled)无法取消
接口地址
POST /api/v1/refunds/{refund_id}/cancel
请求头
Content-Type: application/json
Authorization: Bearer sk_live_xxxxxxxxxx
X-StablePay-Timestamp: 1735123456
X-StablePay-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-StablePay-Signature: 3c8f9d2a7b6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| refund_id | string | 是 | 退款ID |
请求示例
curl -X POST "https://api.stablepay.co/api/v1/refunds/refund_20250101001/cancel" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_live_xxxxxxxxxx" \
-H "X-StablePay-Timestamp: 1735123456" \
-H "X-StablePay-Nonce: 550e8400-e29b-41d4-a716-446655440000" \
-H "X-StablePay-Signature: 3c8f9d2a7b6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0"
响应示例
{
"refund_id": "refund_20250101001",
"status": "canceled",
"canceled_at": 1735123500
}
状态码
200 OK: 取消成功400 Bad Request: 请求参数错误或当前状态不允许取消401 Unauthorized: 认证失败403 Forbidden: 无权取消该退款404 Not Found: 退款不存在
数据结构
Refund 退款对象
interface Refund {
refund_id: string; // Refund ID (provided by merchant)
session_id: string; // Payment session ID
order_id: string; // Order ID
amount: string; // Refund amount (decimal string)
currency: "USDT" | "USDC"; // Currency type
status: RefundStatus; // Refund status
reason?: RefundReason; // Refund reason
description?: string; // Refund description
failure_reason?: string; // Refund failure reason (only when failed)
transaction_hash?: string; // Blockchain transaction hash (only when completed)
receipt_number?: string; // Refund receipt number (only when completed)
created_at: number; // Creation timestamp
processed_at?: number; // Processing completion timestamp
metadata?: Record<string, any>; // Metadata
}
退款状态
退款状态流转图:
pending → processing → completed
↓ ↓ ↓
└───────────┴───────────┘
↓
failed/canceled
状态说明
| 状态 | 值 | 说明 |
|---|---|---|
| 等待退款 | pending | 退款请求已提交,等待处理 |
| 处理中 | processing | 退款正在处理中 |
| 退款完成 | completed | 退款已成功完成,资金已退回 |
| 退款失败 | failed | 退款处理失败 |
| 已取消 | canceled | 退款已被取消 |
状态转换规则
pending→processing: 系统开始处理退款pending→failed: 退款请求被拒绝pending→canceled: 退款被取消processing→completed: 退款处理成功processing→failed: 退款处理失败processing→canceled: 退款被取消
终态状态: completed、failed、canceled 为终态,无法再转换。
退款原因
| 原因值 | 说明 | 使用场景 |
|---|---|---|
requested_by_customer | 客户要求退款 | 客户主动申请退款 |
duplicate | 重复支付 | 客户重复支付了同一订单 |
fraudulent | 欺诈交易 | 检测到欺诈行为 |
expired_uncaptured | 过期未捕获 | 支付过期但未捕获 |
错误码
错误响应格式
{
"error": {
"type": "invalid_request_error",
"code": "REFUND_AMOUNT_EXCEEDED",
"message": "退款金额超过原支付金额",
"param": "amount",
"doc_url": "https://docs.stablepay.co/errors#REFUND_AMOUNT_EXCEEDED"
},
"request_id": "req_xxxxxxxxxxxx",
"timestamp": 1735123456
}
常见错误码
| 错误码 | HTTP状态码 | 说明 |
|---|---|---|
invalid_api_key | 401 | API密钥无效 |
MISSING_CHARGE | 400 | 缺少支付会话ID |
MISSING_REFUND_ID | 400 | 缺少退款ID |
INVALID_JSON | 400 | 请求参数格式错误 |
INVALID_CURRENCY | 400 | 不支持的货币类型 |
INVALID_AMOUNT | 400 | 无效的金额格式或金额必须大于零 |
SESSION_NOT_FOUND | 404 | 支付会话不存在 |
session_access_denied | 403 | 无权访问该支付会话 |
REFUND_NOT_FOUND | 404 | 退款不存在 |
FORBIDDEN | 403 | 无权访问该退款 |
INVALID_SESSION_STATUS | 400 | 只有已完成的支付才能退款 |
REFUND_AMOUNT_EXCEEDED | 400 | 退款金额超过原支付金额 |
REFUND_TIME_EXPIRED | 400 | 退款时间已过期 |
CANNOT_CANCEL_REFUND | 400 | 当前状态不允许取消退款 |
DAILY_REFUND_LIMIT_EXCEEDED | 429 | 超过每日退款限制 |
示例代码
cURL 示例
创建退款
curl -X POST "https://api.stablepay.co/api/v1/refunds/create" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_live_xxxxxxxxxx" \
-d '{
"order_id": "sess_xxxxxxxxxxxx",
"refund_id": "refund_20250101001",
"amount": "50.00",
"currency": "USDT",
"reason": "requested_by_customer",
"description": "客户申请退款"
}'
获取退款信息
curl -X GET "https://api.stablepay.co/api/v1/refunds/refund_20250101001" \
取消退款
curl -X POST "https://api.stablepay.co/api/v1/refunds/refund_20250101001/cancel" \
-H "Content-Type: application/json" \
JavaScript/Node.js 示例
const axios = require('axios');
const API_BASE_URL = 'https://api.stablepay.co/api/v1';
const API_KEY = 'sk_live_xxxxxxxxxx';
// 创建退款
async function createRefund(orderId, refundId, amount, currency, reason) {
try {
const response = await axios.post(
`${API_BASE_URL}/refunds/create`,
{
order_id: orderId,
refund_id: refundId,
amount: amount,
currency: currency,
reason: reason,
description: '客户申请退款'
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
}
}
);
console.log('退款创建成功:', response.data);
return response.data;
} catch (error) {
console.error('创建退款失败:', error.response?.data || error.message);
throw error;
}
}
// 获取退款信息
async function getRefund(refundId) {
try {
const response = await axios.get(
`${API_BASE_URL}/refunds/${refundId}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`
}
}
);
console.log('退款详情:', response.data);
return response.data;
} catch (error) {
console.error('获取退款失败:', error.response?.data || error.message);
throw error;
}
}
// 取消退款
async function cancelRefund(refundId) {
try {
const response = await axios.post(
`${API_BASE_URL}/refunds/${refundId}/cancel`,
{},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
}
}
);
console.log('退款已取消:', response.data);
return response.data;
} catch (error) {
console.error('取消退款失败:', error.response?.data || error.message);
throw error;
}
}
// 使用示例
(async () => {
const orderId = 'sess_xxxxxxxxxxxx'; // 支付会话ID,格式:sess_xxxxxxxxxxxx
const refundId = `refund_${Date.now()}`;
// 创建退款
const refund = await createRefund(
orderId,
refundId,
'50.00',
'USDT',
'requested_by_customer'
);
// 查询退款状态
const refundDetail = await getRefund(refundId);
// 如果退款还在处理中,可以取消
if (refundDetail.status === 'processing') {
await cancelRefund(refundId);
}
})();
Python 示例
import requests
import time
API_BASE_URL = 'https://api.stablepay.co/api/v1'
API_KEY = 'sk_live_xxxxxxxxxx'
def create_refund(order_id, refund_id, amount, currency, reason='requested_by_customer'):
"""创建退款"""
url = f'{API_BASE_URL}/refunds/create'
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}'
}
data = {
'order_id': order_id,
'refund_id': refund_id,
'amount': amount,
'currency': currency,
'reason': reason,
'description': '客户申请退款'
}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_refund(refund_id):
"""获取退款信息"""
url = f'{API_BASE_URL}/refunds/{refund_id}'
headers = {
'Authorization': f'Bearer {API_KEY}'
}
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
def cancel_refund(refund_id):
"""取消退款"""
url = f'{API_BASE_URL}/refunds/{refund_id}/cancel'
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}'
}
response = requests.post(url, headers=headers)
response.raise_for_status()
return response.json()
# 使用示例
if __name__ == '__main__':
order_id = 'sess_xxxxxxxxxxxx' # 支付会话ID,格式:sess_xxxxxxxxxxxx
refund_id = f'refund_{int(time.time())}'
# 创建退款
refund = create_refund(
order_id,
refund_id,
'50.00',
'USDT',
'requested_by_customer'
)
print('退款创建成功:', refund)
# 查询退款状态
refund_detail = get_refund(refund_id)
print('退款详情:', refund_detail)
# 如果退款还在处理中,可以取消
if refund_detail['status'] == 'processing':
canceled_refund = cancel_refund(refund_id)
print('退款已取消:', canceled_refund)