PIX Refund-In (Devolución)
Descripción General
El endpoint PIX Refund-In le permite devolver (reembolsar) pagos PIX recibidos (transacciones del tipo PAYMENT con estado CONFIRMED). Las devoluciones pueden ser totales o parciales y deben solicitarse dentro del plazo de 89 días posteriores a la recepción. El valor devuelto se debita del saldo de su cuenta.
Este endpoint requiere un token Bearer válido. Consulte la documentación de autenticación para más detalles.
Características
- Devoluciones totales o parciales
- Múltiples devoluciones parciales de la misma transacción
- Plazo de hasta 89 días
- Resultado final notificado por el webhook
CashInReversal - Rastreo por motivo de la devolución
Cuándo Usar Devoluciones
Devolución Total
Devuelve el 100% del valor recibido al pagador original.
Casos de uso:
- Cancelación completa del pedido
- Producto no enviado
- Pago duplicado
- Error en el valor cobrado
Devolución Parcial
Devuelve solo una parte del valor recibido.
Casos de uso:
- Devolución de ítems específicos
- Compensación por problemas con el producto/servicio
- Ajuste de valores
- Descuento retroactivo
Endpoint
POST /api/pix/refund-in/{id}
Solicita la devolución de un pago recibido.
Encabezados Requeridos
Authorization: Bearer {token}
Content-Type: application/jsonParámetros de Ruta
idstringobrigatorioIdentificador de la transacción original (Cash-In) a devolver: el transactionId numérico o el End-to-End ID (endToEndId) del PIX recibido.
Ejemplo: "7845"
Cuerpo de la Solicitud
{
"refundValue": 75.00,
"reason": "El cliente solicitó la devolución de 1 ítem del pedido"
}Solicitud
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": "El cliente solicitó la devolución de 1 ítem del pedido"
}'Respuesta (201 Created)
{
"transactionId": "7846",
"externalId": "D123456789",
"status": "PENDING",
"refundValue": 75.00,
"providerTransactionId": "7ef4fc3f-a187-495e-857c-e84d70612761",
"generateTime": "2024-01-19T16:30:00.000Z"
}Parámetros de la Solicitud
refundValuenumberobrigatorioValor a devolver en reales (BRL). Debe tener como máximo 2 decimales.
Validaciones:
- Debe ser mayor o igual a 0.01
- No puede exceder el valor disponible para devolución (
refundableAmountenGET /api/pix/transaction/\{id\}) - La suma de todas las devoluciones no puede exceder el valor original
- La cuenta debe tener saldo disponible para cubrir la devolución
Ejemplo: 75.00
reasonstringMotivo de la devolución (opcional, pero recomendado).
Máximo: 140 caracteres
Ejemplo: "El cliente solicitó la devolución de 1 ítem del pedido"
Recomendación: Proporcione siempre un motivo claro para fines de auditoría
externalIdstringID externo para identificar la devolución (opcional). Debe ser único por cuenta: si se repite, la API devuelve 409 con code PIX_DUPLICATE_EXTERNAL_ID. Cuando se omite, la plataforma genera uno con el formato refund-<uuid>. No puede empezar con MIRROR: (prefijo reservado).
En la API BACEN, corresponde al parámetro 'id' de la URL.
Máximo: 100 caracteres
Ejemplo: "D123456789"
Estructura de la Respuesta
transactionIdstringsempre presenteID de la nueva transacción de devolución generada.
Ejemplo: "7846"
Nota: Es un ID distinto al de la transacción original
externalIdstringsempre presenteID externo de la transacción de devolución.
Ejemplo: "D123456789"
statusstringsempre presenteEstado actual de la transacción de devolución.
Valores posibles:
PENDING: Devolución en procesamientoCONFIRMED: Devolución confirmada y finalizadaERROR: Error en el procesamiento
Ejemplo: "PENDING"
refundValuenumbersempre presenteValor de la devolución en reales.
Ejemplo: 75.00
providerTransactionIdstringsempre presenteID de la transacción en el proveedor (usado para la correlación con webhooks).
Ejemplo: "7ef4fc3f-a187-495e-857c-e84d70612761"
generateTimestringsempre presenteFecha y hora de generación de la devolución (ISO 8601 UTC).
Ejemplo: "2024-01-19T16:30:00.000Z"
Ejemplos de Implementación
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 || 'Devolución solicitada por el cliente'
};
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('¡Devolución PIX iniciada con éxito!');
console.log(`ID de la Transacción de Devolución: ${response.data.transactionId}`);
console.log(`ID Externo Original: ${response.data.externalId}`);
console.log(`Valor de la Devolución: R$ ${response.data.refundValue.toFixed(2)}`);
console.log(`Estado: ${response.data.status}`);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const errorData = error.response?.data;
console.error('Error al procesar la devolución:', errorData);
// Tratar errores específicos
if (error.response?.status === 400) {
if (errorData?.code === 'PIX_REFUND_DEADLINE_EXCEEDED') {
throw new Error('Se excedió el plazo de 89 días para la devolución');
}
if (errorData?.code === 'PIX_REFUND_EXCEEDS_AVAILABLE') {
throw new Error('El valor de la devolución excede el disponible para devolución');
}
}
if (error.response?.status === 404) {
throw new Error('Transacción original no encontrada');
}
throw new Error(errorData?.message || 'Error al procesar la devolución');
}
throw error;
}
}
// Uso - Devolución Total
async function fullRefund(token: string, transactionId: string, originalValue: number) {
return await refundPixPayment(
token,
transactionId,
originalValue,
'Cancelación total del pedido'
);
}
// Uso - Devolución Parcial
async function partialRefund(token: string, transactionId: string, itemValue: number) {
return await refundPixPayment(
token,
transactionId,
itemValue,
'Devolución de 1 ítem del pedido'
);
}
// Ejemplo práctico
const token = 'su_token_aqui';
const transactionId = '7845';
// Devolver R$ 75,00 de una transacción de R$ 150,00
refundPixPayment(token, transactionId, 75.00, 'El cliente solicitó una devolución parcial');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:
"""
Devuelve un pago PIX recibido
Args:
token: Token Bearer válido
original_transaction_id: ID de la transacción original (Cash-In)
refund_amount: Valor a devolver
reason: Motivo de la devolución (opcional)
Returns:
Datos de la devolución creada
"""
url = f'https://api.fypay.me/api/pix/refund-in/{original_transaction_id}'
payload = {
'refundValue': round(refund_amount, 2),
'reason': reason or 'Devolución solicitada por el cliente'
}
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('¡Devolución PIX iniciada con éxito!')
print(f"ID de la Transacción de Devolución: {data['transactionId']}")
print(f"ID Externo Original: {data['externalId']}")
print(f"Valor de la Devolución: R$ {data['refundValue']:.2f}")
print(f"Estado: {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))
# Tratar errores específicos
if e.response.status_code == 400:
if error_data.get('code') == 'PIX_REFUND_DEADLINE_EXCEEDED':
raise Exception('Se excedió el plazo de 89 días para la devolución')
if error_data.get('code') == 'PIX_REFUND_EXCEEDS_AVAILABLE':
raise Exception('El valor de la devolución excede el disponible para devolución')
raise Exception(f'Datos inválidos: {error_message}')
if e.response.status_code == 404:
raise Exception('Transacción original no encontrada')
raise Exception(f'Error al procesar la devolución: {error_message}')
# Uso
token = 'su_token_aqui'
transaction_id = '7845'
# Devolución parcial
refund = refund_pix_payment(
token=token,
original_transaction_id=transaction_id,
refund_amount=75.00,
reason='El cliente solicitó la devolución de 1 ítem del pedido'
)
# Devolución total
def full_refund(token: str, transaction_id: str, original_value: float):
"""Realiza una devolución total"""
return refund_pix_payment(
token=token,
original_transaction_id=transaction_id,
refund_amount=original_value,
reason='Cancelación total del pedido'
)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 ?? 'Devolución solicitada por el cliente'
];
$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('Se excedió el plazo de 89 días para la devolución');
}
if (($errorData['code'] ?? '') === 'PIX_REFUND_EXCEEDS_AVAILABLE') {
throw new Exception('El valor de la devolución excede el disponible para devolución');
}
}
if ($httpCode === 404) {
throw new Exception('Transacción original no encontrada');
}
throw new Exception("Error al procesar la devolución: $errorMessage");
}
$data = json_decode($response, true);
echo "¡Devolución PIX iniciada con éxito!" . PHP_EOL;
echo "ID de la Transacción de Devolución: {$data['transactionId']}" . PHP_EOL;
echo "ID Externo Original: {$data['externalId']}" . PHP_EOL;
echo "Valor de la Devolución: R$ " . number_format($data['refundValue'], 2, ',', '.') . PHP_EOL;
echo "Estado: {$data['status']}" . PHP_EOL;
return $data;
}
// Uso
$token = 'su_token_aqui';
$transactionId = '7845';
// Devolución parcial
$refund = refundPixPayment(
$token,
$transactionId,
75.00,
'El cliente solicitó la devolución de 1 ítem del pedido'
);Casos de Uso
1. E-commerce - Devolución de Productos
class OrderRefundSystem {
constructor(private token: string) {}
async processItemReturn(orderId: string, returnedItems: OrderItem[]) {
// Buscar la transacción original del pedido
const originalTransaction = await this.getTransactionByOrderId(orderId);
// Calcular el valor total a devolver
const refundAmount = returnedItems.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
// Verificar que no exceda el valor de la transacción original
const availableForRefund = await this.getAvailableRefundAmount(
originalTransaction.id
);
if (refundAmount > availableForRefund) {
throw new Error(
`El valor solicitado (R$ ${refundAmount.toFixed(2)}) excede el disponible ` +
`para devolución (R$ ${availableForRefund.toFixed(2)})`
);
}
// Generar la descripción de la devolución
const itemsDescription = returnedItems
.map(item => `${item.name} (${item.quantity}x)`)
.join(', ');
// Realizar la devolución
const refund = await refundPixPayment(
this.token,
originalTransaction.id,
refundAmount,
`Devolución de ítems: ${itemsDescription}`
);
// Actualizar el estado del pedido
await this.updateOrderStatus(orderId, 'PARTIALLY_REFUNDED', refund);
// Notificar al cliente
await this.notifyCustomerRefund(orderId, refundAmount);
return refund;
}
async getAvailableRefundAmount(transactionId: string): Promise<number> {
// GET /api/pix/transaction/{id} ya trae el saldo devolvible
const transaction = await this.getTransaction(transactionId);
return transaction.refundableAmount;
}
}
// Uso
interface OrderItem {
name: string;
price: number;
quantity: number;
}
const refundSystem = new OrderRefundSystem('su_token_aqui');
const returnedItems: OrderItem[] = [
{ name: 'Camiseta Azul', price: 49.90, quantity: 1 }
];
await refundSystem.processItemReturn('ORDER-12345', returnedItems);2. SaaS - Reembolso Proporcional
from datetime import datetime, timedelta
from decimal import Decimal
class SubscriptionRefundManager:
"""Gestiona reembolsos proporcionales de suscripciones"""
def __init__(self, token: str):
self.token = token
def calculate_prorated_refund(
self,
payment_date: datetime,
cancellation_date: datetime,
monthly_value: float
) -> float:
"""Calcula el reembolso proporcional basado en los días no utilizados"""
# Calcular los días de la mensualidad (30 días)
billing_period_days = 30
# Calcular los días utilizados
days_used = (cancellation_date - payment_date).days
# Calcular los días no utilizados
days_unused = billing_period_days - days_used
if days_unused <= 0:
return 0.0
# Calcular el valor proporcional
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:
"""Procesa la cancelación con reembolso proporcional"""
# Buscar los datos de la suscripción
subscription = self.get_subscription(subscription_id)
# Calcular el reembolso proporcional
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': 'Sin valor a reembolsar'}
# Realizar la devolución
refund = refund_pix_payment(
token=self.token,
original_transaction_id=transaction_id,
refund_amount=refund_amount,
reason=f'Cancelación de suscripción - Reembolso proporcional'
)
# Actualizar el estado de la suscripción
self.update_subscription_status(subscription_id, 'CANCELLED')
return refund
# Uso
manager = SubscriptionRefundManager('su_token_aqui')
# El cliente pagó R$ 99,00 el 01/01 y canceló el 15/01
# Reembolso proporcional: 15 días no utilizados
refund = manager.process_subscription_cancellation(
subscription_id='SUB-12345',
transaction_id='7845'
)3. Marketplace - Compensación por Problemas
class MarketplaceCompensation {
constructor(token) {
this.token = token;
}
async compensateForIssue(orderId, issueType) {
const order = await this.getOrder(orderId);
const compensationRules = this.getCompensationRules();
// Definir el valor de la compensación según el tipo de problema
const compensationPercent = compensationRules[issueType] || 0;
const compensationAmount = order.value * (compensationPercent / 100);
if (compensationAmount === 0) {
throw new Error('Tipo de problema no elegible para compensación');
}
// Realizar una devolución parcial como compensación
const refund = await refundPixPayment(
this.token,
order.transactionId,
compensationAmount,
`Compensación por ${issueType} - ${compensationPercent}% de descuento`
);
// Registrar la compensación
await this.recordCompensation(orderId, issueType, compensationAmount);
return refund;
}
getCompensationRules() {
return {
'ATRASO_ENTREGA': 10, // 10% de compensación
'PRODUTO_AVARIADO': 20, // 20% de compensación
'ITEM_FALTANTE': 15, // 15% de compensación
'QUALIDADE_INFERIOR': 25 // 25% de compensación
};
}
}
// Uso
const compensation = new MarketplaceCompensation('su_token_aqui');
// El producto llegó dañado - compensar con 20%
await compensation.compensateForIssue('ORDER-12345', 'PRODUTO_AVARIADO');Validaciones y Reglas de Negocio
Verificar el Valor Disponible para Devolución
El GET /api/pix/transaction/\{id\} de la transacción original ya trae refundedAmount (suma de las devoluciones confirmadas), refundableAmount (cuánto aún puede devolverse) y relatedTransactions (las devoluciones vinculadas):
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(
`El valor solicitado (R$ ${requestedAmount.toFixed(2)}) excede el disponible ` +
`para devolución (R$ ${transaction.refundableAmount.toFixed(2)}). ` +
`Total ya devuelto: R$ ${transaction.refundedAmount.toFixed(2)}`
);
}
return true;
}Verificar el Plazo de Devolución
from datetime import datetime, timedelta
def can_refund_transaction(transaction_date: datetime) -> bool:
"""Verifica si la transacción aún está dentro del plazo de devolución"""
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'Plazo de devolución excedido. '
f'Transacción realizada hace {days_passed} días. '
f'Plazo máximo: {max_refund_days} días.'
)
return True
# Uso
try:
can_refund_transaction(datetime(2024, 1, 1))
print('La transacción puede devolverse')
except Exception as e:
print(f'Error: {e}')Monitoreo de Devoluciones
El resultado final de la devolución llega por el webhook CashInReversal (evento refund_in en la configuración de webhooks). Como alternativa, consulte GET /api/pix/transaction/\{id\} con el transactionId de la devolución:
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('¡Devolución confirmada!');
await this.onRefundConfirmed(refundTransactionId);
return true;
}
if (status === 'ERROR') {
await this.onRefundFailed(refundTransactionId);
throw new Error('La devolución falló');
}
// Esperar 3 segundos antes de verificar de nuevo
await new Promise(resolve => setTimeout(resolve, 3000));
}
throw new Error('Timeout: Devolución no confirmada en el tiempo esperado');
}
async onRefundConfirmed(refundTransactionId: string) {
// Actualizar la base de datos
// Notificar al cliente
// Registrar log
}
async onRefundFailed(refundTransactionId: string) {
// Notificar al equipo de soporte
// Registrar el incidente
// Crear un ticket para análisis manual
}
}Códigos de Respuesta
| Código | Descripción | Significado |
|---|---|---|
201 | Devolución Creada | Devolución PIX iniciada con éxito |
400 | Valor Inválido | PIX_REFUND_EXCEEDS_AVAILABLE (valor por encima del disponible) o PIX_INVALID_REFUND_AMOUNT |
400 | Plazo Excedido | PIX_REFUND_DEADLINE_EXCEEDED: se excedió el plazo de 89 días para la devolución |
400 | Transacción no devolvible | PIX_INVALID_TRANSACTION_TYPE_FOR_REFUND (no es una recepción) o PIX_INVALID_TRANSACTION_STATE (recepción aún no confirmada) |
400 | Saldo Insuficiente | PIX_INSUFFICIENT_BALANCE: la cuenta no tiene saldo para devolver el valor |
401 | Token Inválido | Token no proporcionado, expirado o inválido |
404 | Transacción No Encontrada | PIX_TRANSACTION_NOT_FOUND: transacción original no encontrada |
409 | externalId Repetido | PIX_DUPLICATE_EXTERNAL_ID: ya existe una devolución con ese externalId |
409 | Devolución Bloqueada | PIX_REFUND_BLOCKED_MISMATCH_HELD: transacción en revisión por divergencia de valor; la devolución queda disponible después de la confirmación del valor recibido |
Consulte la Referencia de la API para detalles completos de los campos de respuesta.
Mejores Prácticas
Notas Importantes
Las devoluciones no pueden cancelarse una vez iniciadas. Asegúrese de los valores antes de procesar.
- Plazo máximo: 89 días después de la recepción
- Valor mínimo: R$ 0,01
- Múltiples devoluciones: Permitidas, siempre que la suma no exceda el valor original