docs
集成指南

PIX Refund-In(退款)

概述

PIX Refund-In 端点允许您退还(退回)已收到的 PIX 付款(类型为 PAYMENT 且状态为 CONFIRMED 的交易)。退款可以是全额或部分退款,必须在收款后 89 天内申请。退款金额从您的账户余额中扣除。

此端点需要有效的 Bearer token。详情请参阅认证文档。

功能特性

  • 全额或部分退款
  • 同一交易支持多次部分退款
  • 最长 89 天的期限
  • 最终结果通过 CashInReversal webhook 通知
  • 按退款原因追踪

何时使用退款

全额退款

将收到的全部金额退还给原始付款人。

使用场景:

  • 完全取消订单
  • 商品未发货
  • 重复付款
  • 收款金额错误

部分退款

仅退还收到金额的一部分。

使用场景:

  • 退回特定商品
  • 对商品/服务问题进行补偿
  • 金额调整
  • 追溯折扣

端点

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无效 TokenToken 未提供、已过期或无效
404未找到交易PIX_TRANSACTION_NOT_FOUND:未找到原始交易
409externalId 重复PIX_DUPLICATE_EXTERNAL_ID:已存在使用该 externalId 的退款
409退款被锁定PIX_REFUND_BLOCKED_MISMATCH_HELD:交易正处于金额差异审核中;收款金额确认后即可退款

请参阅 API 参考 获取响应字段的完整详情。

最佳实践

重要说明

退款一旦发起便无法取消。处理前请确认金额无误。

  • 最长期限: 收款后 89 天
  • 最低金额: R$ 0,01
  • 多次退款: 允许,只要总和不超过原始金额

后续步骤

本页目录