PIX Refund-In(退款)
概述
PIX Refund-In 端点允许您退还(退回)已收到的 PIX 付款(类型为 PAYMENT 且状态为 CONFIRMED 的交易)。退款可以是全额或部分退款,必须在收款后 89 天内申请。退款金额从您的账户余额中扣除。
此端点需要有效的 Bearer token。详情请参阅认证文档。
功能特性
- 全额或部分退款
- 同一交易支持多次部分退款
- 最长 89 天的期限
- 最终结果通过
CashInReversalwebhook 通知 - 按退款原因追踪
何时使用退款
全额退款
将收到的全部金额退还给原始付款人。
使用场景:
- 完全取消订单
- 商品未发货
- 重复付款
- 收款金额错误
部分退款
仅退还收到金额的一部分。
使用场景:
- 退回特定商品
- 对商品/服务问题进行补偿
- 金额调整
- 追溯折扣
端点
POST /api/pix/refund-in/{id}
为已收到的付款申请退款。
必需请求头
Authorization: Bearer {token}
Content-Type: application/json路径参数
idstringobrigatorio要退款的原始交易(Cash-In)的标识符:数字 transactionId 或所收 PIX 的 End-to-End ID(endToEndId)。
示例: "7845"
请求体
{
"refundValue": 75.00,
"reason": "客户申请退回订单中的 1 件商品"
}请求
curl -X POST https://api.fypay.me/api/pix/refund-in/7845 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"refundValue": 75.00,
"reason": "客户申请退回订单中的 1 件商品"
}'响应 (201 Created)
{
"transactionId": "7846",
"externalId": "D123456789",
"status": "PENDING",
"refundValue": 75.00,
"providerTransactionId": "7ef4fc3f-a187-495e-857c-e84d70612761",
"generateTime": "2024-01-19T16:30:00.000Z"
}请求参数
refundValuenumberobrigatorio要退款的金额,单位为巴西雷亚尔(BRL)。最多 2 位小数。
验证规则:
- 必须大于或等于 0.01
- 不能超过可退款金额(
GET /api/pix/transaction/\{id\}中的refundableAmount) - 所有退款的总和不能超过原始金额
- 账户必须有足够的可用余额来支付该退款
示例: 75.00
reasonstring退款原因(可选,但建议填写)。
最大长度: 140 个字符
示例: "客户申请退回订单中的 1 件商品"
建议: 出于审计目的,请始终提供清晰的原因
externalIdstring用于识别该退回的外部 ID(可选)。在每个账户内必须唯一:如果重复,API 返回 409,code 为 PIX_DUPLICATE_EXTERNAL_ID。省略时,平台会以 refund-<uuid> 格式生成一个。不能以 MIRROR: 开头(保留前缀)。
在 BACEN API 中,对应 URL 中的 'id' 参数。
最大长度: 100 个字符
示例: "D123456789"
响应结构
transactionIdstringsempre presente生成的新退款交易的 ID。
示例: "7846"
注意: 该 ID 与原始交易的 ID 不同
externalIdstringsempre presente退款交易的外部 ID。
示例: "D123456789"
statusstringsempre presente退款交易的当前状态。
可能的值:
PENDING:退款处理中CONFIRMED:退款已确认并完成ERROR:处理出错
示例: "PENDING"
refundValuenumbersempre presente退款金额(雷亚尔)。
示例: 75.00
providerTransactionIdstringsempre presente该交易在提供商处的 ID(用于与 webhook 关联)。
示例: "7ef4fc3f-a187-495e-857c-e84d70612761"
generateTimestringsempre presente退款生成的日期和时间(ISO 8601 UTC)。
示例: "2024-01-19T16:30:00.000Z"
实现示例
Node.js / TypeScript
import axios from 'axios';
interface RefundRequest {
refundValue: number;
reason?: string;
externalId?: string;
}
interface RefundResponse {
transactionId: string;
externalId: string;
status: 'PENDING' | 'CONFIRMED' | 'ERROR';
refundValue: number;
providerTransactionId: string;
generateTime: string;
}
async function refundPixPayment(
token: string,
originalTransactionId: string,
refundAmount: number,
reason?: string
): Promise<RefundResponse> {
const payload: RefundRequest = {
refundValue: refundAmount,
reason: reason || '客户申请的退款'
};
try {
const response = await axios.post<RefundResponse>(
`https://api.fypay.me/api/pix/refund-in/${originalTransactionId}`,
payload,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log('PIX 退款已成功发起!');
console.log(`退款交易 ID:${response.data.transactionId}`);
console.log(`原始外部 ID:${response.data.externalId}`);
console.log(`退款金额:R$ ${response.data.refundValue.toFixed(2)}`);
console.log(`状态:${response.data.status}`);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const errorData = error.response?.data;
console.error('处理退款出错:', errorData);
// 处理特定错误
if (error.response?.status === 400) {
if (errorData?.code === 'PIX_REFUND_DEADLINE_EXCEEDED') {
throw new Error('已超过 89 天的退款期限');
}
if (errorData?.code === 'PIX_REFUND_EXCEEDS_AVAILABLE') {
throw new Error('退款金额超过可退款金额');
}
}
if (error.response?.status === 404) {
throw new Error('未找到原始交易');
}
throw new Error(errorData?.message || '处理退款出错');
}
throw error;
}
}
// 用法 - 全额退款
async function fullRefund(token: string, transactionId: string, originalValue: number) {
return await refundPixPayment(
token,
transactionId,
originalValue,
'订单全部取消'
);
}
// 用法 - 部分退款
async function partialRefund(token: string, transactionId: string, itemValue: number) {
return await refundPixPayment(
token,
transactionId,
itemValue,
'退回订单中的 1 件商品'
);
}
// 实际示例
const token = 'your_token_here';
const transactionId = '7845';
// 从一笔 R$ 150,00 的交易中退款 R$ 75,00
refundPixPayment(token, transactionId, 75.00, '客户申请部分退款');Python
import requests
from datetime import datetime
from typing import Dict, Optional
def refund_pix_payment(
token: str,
original_transaction_id: str,
refund_amount: float,
reason: Optional[str] = None
) -> Dict:
"""
退还已收到的 PIX 付款
Args:
token: 有效的 Bearer token
original_transaction_id: 原始交易(Cash-In)的 ID
refund_amount: 要退款的金额
reason: 退款原因(可选)
Returns:
已创建退款的数据
"""
url = f'https://api.fypay.me/api/pix/refund-in/{original_transaction_id}'
payload = {
'refundValue': round(refund_amount, 2),
'reason': reason or '客户申请的退款'
}
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
try:
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
print('PIX 退款已成功发起!')
print(f"退款交易 ID:{data['transactionId']}")
print(f"原始外部 ID:{data['externalId']}")
print(f"退款金额:R$ {data['refundValue']:.2f}")
print(f"状态:{data['status']}")
return data
except requests.exceptions.HTTPError as e:
error_data = e.response.json() if e.response else {}
error_message = error_data.get('message', str(e))
# 处理特定错误
if e.response.status_code == 400:
if error_data.get('code') == 'PIX_REFUND_DEADLINE_EXCEEDED':
raise Exception('已超过 89 天的退款期限')
if error_data.get('code') == 'PIX_REFUND_EXCEEDS_AVAILABLE':
raise Exception('退款金额超过可退款金额')
raise Exception(f'数据无效:{error_message}')
if e.response.status_code == 404:
raise Exception('未找到原始交易')
raise Exception(f'处理退款出错:{error_message}')
# 用法
token = 'your_token_here'
transaction_id = '7845'
# 部分退款
refund = refund_pix_payment(
token=token,
original_transaction_id=transaction_id,
refund_amount=75.00,
reason='客户申请退回订单中的 1 件商品'
)
# 全额退款
def full_refund(token: str, transaction_id: str, original_value: float):
"""执行全额退款"""
return refund_pix_payment(
token=token,
original_transaction_id=transaction_id,
refund_amount=original_value,
reason='订单全部取消'
)PHP
<?php
function refundPixPayment(
string $token,
string $originalTransactionId,
float $refundAmount,
?string $reason = null
): array {
$url = "https://api.fypay.me/api/pix/refund-in/$originalTransactionId";
$payload = [
'refundValue' => round($refundAmount, 2),
'reason' => $reason ?? '客户申请的退款'
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 201) {
$errorData = json_decode($response, true);
$errorMessage = $errorData['message'] ?? "HTTP $httpCode";
if ($httpCode === 400) {
if (($errorData['code'] ?? '') === 'PIX_REFUND_DEADLINE_EXCEEDED') {
throw new Exception('已超过 89 天的退款期限');
}
if (($errorData['code'] ?? '') === 'PIX_REFUND_EXCEEDS_AVAILABLE') {
throw new Exception('退款金额超过可退款金额');
}
}
if ($httpCode === 404) {
throw new Exception('未找到原始交易');
}
throw new Exception("处理退款出错:$errorMessage");
}
$data = json_decode($response, true);
echo "PIX 退款已成功发起!" . PHP_EOL;
echo "退款交易 ID:{$data['transactionId']}" . PHP_EOL;
echo "原始外部 ID:{$data['externalId']}" . PHP_EOL;
echo "退款金额:R$ " . number_format($data['refundValue'], 2, ',', '.') . PHP_EOL;
echo "状态:{$data['status']}" . PHP_EOL;
return $data;
}
// 用法
$token = 'your_token_here';
$transactionId = '7845';
// 部分退款
$refund = refundPixPayment(
$token,
$transactionId,
75.00,
'客户申请退回订单中的 1 件商品'
);使用场景
1. 电商 - 商品退货
class OrderRefundSystem {
constructor(private token: string) {}
async processItemReturn(orderId: string, returnedItems: OrderItem[]) {
// 查找订单的原始交易
const originalTransaction = await this.getTransactionByOrderId(orderId);
// 计算需退款的总金额
const refundAmount = returnedItems.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
// 检查是否超过原始交易金额
const availableForRefund = await this.getAvailableRefundAmount(
originalTransaction.id
);
if (refundAmount > availableForRefund) {
throw new Error(
`申请金额(R$ ${refundAmount.toFixed(2)})超过可退款` +
`金额(R$ ${availableForRefund.toFixed(2)})`
);
}
// 生成退款描述
const itemsDescription = returnedItems
.map(item => `${item.name} (${item.quantity}x)`)
.join(', ');
// 执行退款
const refund = await refundPixPayment(
this.token,
originalTransaction.id,
refundAmount,
`商品退货:${itemsDescription}`
);
// 更新订单状态
await this.updateOrderStatus(orderId, 'PARTIALLY_REFUNDED', refund);
// 通知客户
await this.notifyCustomerRefund(orderId, refundAmount);
return refund;
}
async getAvailableRefundAmount(transactionId: string): Promise<number> {
// GET /api/pix/transaction/{id} 已返回可退款余额
const transaction = await this.getTransaction(transactionId);
return transaction.refundableAmount;
}
}
// 用法
interface OrderItem {
name: string;
price: number;
quantity: number;
}
const refundSystem = new OrderRefundSystem('your_token_here');
const returnedItems: OrderItem[] = [
{ name: '蓝色 T 恤', price: 49.90, quantity: 1 }
];
await refundSystem.processItemReturn('ORDER-12345', returnedItems);2. SaaS - 按比例退款
from datetime import datetime, timedelta
from decimal import Decimal
class SubscriptionRefundManager:
"""管理订阅的按比例退款"""
def __init__(self, token: str):
self.token = token
def calculate_prorated_refund(
self,
payment_date: datetime,
cancellation_date: datetime,
monthly_value: float
) -> float:
"""根据未使用的天数计算按比例退款"""
# 计算月费周期的天数(30 天)
billing_period_days = 30
# 计算已使用的天数
days_used = (cancellation_date - payment_date).days
# 计算未使用的天数
days_unused = billing_period_days - days_used
if days_unused <= 0:
return 0.0
# 计算按比例金额
daily_rate = Decimal(str(monthly_value)) / Decimal(str(billing_period_days))
refund_amount = float(daily_rate * Decimal(str(days_unused)))
return round(refund_amount, 2)
def process_subscription_cancellation(
self,
subscription_id: str,
transaction_id: str
) -> dict:
"""处理带按比例退款的取消"""
# 获取订阅数据
subscription = self.get_subscription(subscription_id)
# 计算按比例退款
refund_amount = self.calculate_prorated_refund(
payment_date=subscription['last_payment_date'],
cancellation_date=datetime.now(),
monthly_value=subscription['monthly_value']
)
if refund_amount <= 0:
return {'refund': None, 'message': '没有可退款的金额'}
# 执行退款
refund = refund_pix_payment(
token=self.token,
original_transaction_id=transaction_id,
refund_amount=refund_amount,
reason=f'取消订阅 - 按比例退款'
)
# 更新订阅状态
self.update_subscription_status(subscription_id, 'CANCELLED')
return refund
# 用法
manager = SubscriptionRefundManager('your_token_here')
# 客户于 01/01 支付了 R$ 99,00,并于 15/01 取消
# 按比例退款:15 天未使用
refund = manager.process_subscription_cancellation(
subscription_id='SUB-12345',
transaction_id='7845'
)3. 电商平台 - 问题补偿
class MarketplaceCompensation {
constructor(token) {
this.token = token;
}
async compensateForIssue(orderId, issueType) {
const order = await this.getOrder(orderId);
const compensationRules = this.getCompensationRules();
// 根据问题类型确定补偿金额
const compensationPercent = compensationRules[issueType] || 0;
const compensationAmount = order.value * (compensationPercent / 100);
if (compensationAmount === 0) {
throw new Error('该问题类型不符合补偿条件');
}
// 以部分退款的方式进行补偿
const refund = await refundPixPayment(
this.token,
order.transactionId,
compensationAmount,
`因 ${issueType} 进行补偿 - ${compensationPercent}% 折扣`
);
// 记录补偿
await this.recordCompensation(orderId, issueType, compensationAmount);
return refund;
}
getCompensationRules() {
return {
'ATRASO_ENTREGA': 10, // 10% 补偿
'PRODUTO_AVARIADO': 20, // 20% 补偿
'ITEM_FALTANTE': 15, // 15% 补偿
'QUALIDADE_INFERIOR': 25 // 25% 补偿
};
}
}
// 用法
const compensation = new MarketplaceCompensation('your_token_here');
// 商品到货时已损坏 - 补偿 20%
await compensation.compensateForIssue('ORDER-12345', 'PRODUTO_AVARIADO');验证与业务规则
检查可退款金额
原始交易的 GET /api/pix/transaction/\{id\} 已返回 refundedAmount(已确认退款的总和)、refundableAmount(仍可退款的金额)和 relatedTransactions(关联的退款):
async function validateRefundAmount(
token: string,
transactionId: string,
requestedAmount: number
): Promise<boolean> {
const response = await fetch(
`https://api.fypay.me/api/pix/transaction/${transactionId}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const transaction = await response.json();
if (requestedAmount > transaction.refundableAmount) {
throw new Error(
`申请金额(R$ ${requestedAmount.toFixed(2)})超过可退款` +
`金额(R$ ${transaction.refundableAmount.toFixed(2)})。` +
`已退款总额:R$ ${transaction.refundedAmount.toFixed(2)}`
);
}
return true;
}检查退款期限
from datetime import datetime, timedelta
def can_refund_transaction(transaction_date: datetime) -> bool:
"""检查交易是否仍在退款期限内"""
max_refund_days = 89
cutoff_date = datetime.now() - timedelta(days=max_refund_days)
if transaction_date < cutoff_date:
days_passed = (datetime.now() - transaction_date).days
raise Exception(
f'已超过退款期限。'
f'交易发生于 {days_passed} 天前。'
f'最长期限:{max_refund_days} 天。'
)
return True
# 用法
try:
can_refund_transaction(datetime(2024, 1, 1))
print('该交易可以退款')
except Exception as e:
print(f'错误:{e}')退款监控
退款的最终结果通过 CashInReversal webhook 送达(webhook 配置中的 refund_in 事件)。或者,使用退款的 transactionId 查询 GET /api/pix/transaction/\{id\}:
class RefundMonitor {
async monitorRefundStatus(refundTransactionId: string, timeout = 60000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const status = await this.checkRefundStatus(refundTransactionId);
if (status === 'CONFIRMED') {
console.log('退款已确认!');
await this.onRefundConfirmed(refundTransactionId);
return true;
}
if (status === 'ERROR') {
await this.onRefundFailed(refundTransactionId);
throw new Error('退款失败');
}
// 等待 3 秒后再次检查
await new Promise(resolve => setTimeout(resolve, 3000));
}
throw new Error('超时:退款未在预期时间内确认');
}
async onRefundConfirmed(refundTransactionId: string) {
// 更新数据库
// 通知客户
// 记录日志
}
async onRefundFailed(refundTransactionId: string) {
// 通知支持团队
// 记录事件
// 创建工单以进行人工分析
}
}响应状态码
| 状态码 | 描述 | 含义 |
|---|---|---|
201 | 退款已创建 | PIX 退款已成功发起 |
400 | 金额无效 | PIX_REFUND_EXCEEDS_AVAILABLE(金额超过可退款金额)或 PIX_INVALID_REFUND_AMOUNT |
400 | 超过期限 | PIX_REFUND_DEADLINE_EXCEEDED:已超过 89 天的退款期限 |
400 | 交易不可退款 | PIX_INVALID_TRANSACTION_TYPE_FOR_REFUND(不是收款)或 PIX_INVALID_TRANSACTION_STATE(收款尚未确认) |
400 | 余额不足 | PIX_INSUFFICIENT_BALANCE:账户余额不足以退还该金额 |
401 | 无效 Token | Token 未提供、已过期或无效 |
404 | 未找到交易 | PIX_TRANSACTION_NOT_FOUND:未找到原始交易 |
409 | externalId 重复 | PIX_DUPLICATE_EXTERNAL_ID:已存在使用该 externalId 的退款 |
409 | 退款被锁定 | PIX_REFUND_BLOCKED_MISMATCH_HELD:交易正处于金额差异审核中;收款金额确认后即可退款 |
请参阅 API 参考 获取响应字段的完整详情。
最佳实践
重要说明
退款一旦发起便无法取消。处理前请确认金额无误。
- 最长期限: 收款后 89 天
- 最低金额: R$ 0,01
- 多次退款: 允许,只要总和不超过原始金额