PIX Refund-In (Reversal)
Overview
The PIX Refund-In endpoint allows you to reverse (refund) received PIX payments (transactions of type PAYMENT with status CONFIRMED). Refunds can be full or partial and must be requested within 89 days of receipt. The refunded amount is debited from your account balance.
This endpoint requires a valid Bearer token. See the authentication documentation for more details.
Features
- Full or partial refunds
- Multiple partial refunds for the same transaction
- Up to 89-day window
- Final result notified by the
CashInReversalwebhook - Tracking by refund reason
When to Use Refunds
Full Refund
Returns 100% of the received amount to the original payer.
Use cases:
- Complete order cancellation
- Product not shipped
- Duplicate payment
- Incorrect charge amount
Partial Refund
Returns only part of the received amount.
Use cases:
- Return of specific items
- Compensation for product/service issues
- Value adjustments
- Retroactive discount
Endpoint
POST /api/pix/refund-in/{id}
Requests the refund of a received payment.
Required Headers
Authorization: Bearer {token}
Content-Type: application/jsonPath Parameters
idstringobrigatorioIdentifier of the original transaction (Cash-In) to be refunded: the numeric transactionId or the End-to-End ID (endToEndId) of the received PIX.
Example: "7845"
Request Body
{
"refundValue": 75.00,
"reason": "Cliente solicitou devolução de 1 item do pedido"
}Request
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": "Cliente solicitou devolução de 1 item do pedido"
}'Response (201 Created)
{
"transactionId": "7846",
"externalId": "D123456789",
"status": "PENDING",
"refundValue": 75.00,
"providerTransactionId": "7ef4fc3f-a187-495e-857c-e84d70612761",
"generateTime": "2024-01-19T16:30:00.000Z"
}Request Parameters
refundValuenumberobrigatorioAmount to be refunded in BRL (Brazilian Reais). Must have at most 2 decimal places.
Validations:
- Must be greater than or equal to 0.01
- Cannot exceed the amount available for refund (
refundableAmountinGET /api/pix/transaction/\{id\}) - Sum of all refunds cannot exceed the original amount
- The account must have available balance to cover the refund
Example: 75.00
reasonstringRefund reason (optional, but recommended).
Maximum: 140 characters
Example: "Cliente solicitou devolução de 1 item do pedido"
Recommendation: Always provide a clear reason for auditing purposes
externalIdstringExternal ID for refund identification (optional). Must be unique per account: if repeated, the API returns 409 with code PIX_DUPLICATE_EXTERNAL_ID. When omitted, the platform generates one in the refund-<uuid> format. Cannot start with MIRROR: (reserved prefix).
In the BACEN API, corresponds to the 'id' URL parameter.
Maximum: 100 characters
Example: "D123456789"
Response Structure
transactionIdstringsempre presenteID of the new refund transaction generated.
Example: "7846"
Note: This is a different ID from the original transaction
externalIdstringsempre presenteExternal ID of the refund transaction.
Example: "D123456789"
statusstringsempre presenteCurrent refund transaction status.
Possible values:
PENDING: Refund being processedCONFIRMED: Refund confirmed and completedERROR: Processing error
Example: "PENDING"
refundValuenumbersempre presenteRefund amount in BRL.
Example: 75.00
providerTransactionIdstringsempre presenteTransaction ID at the provider (used for correlation with webhooks).
Example: "7ef4fc3f-a187-495e-857c-e84d70612761"
generateTimestringsempre presenteRefund generation date and time (ISO 8601 UTC).
Example: "2024-01-19T16:30:00.000Z"
Implementation Examples
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 || 'Estorno solicitado pelo 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('PIX refund initiated successfully!');
console.log(`Refund Transaction ID: ${response.data.transactionId}`);
console.log(`Original External ID: ${response.data.externalId}`);
console.log(`Refund Amount: R$ ${response.data.refundValue.toFixed(2)}`);
console.log(`Status: ${response.data.status}`);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const errorData = error.response?.data;
console.error('Error processing refund:', errorData);
// Handle specific errors
if (error.response?.status === 400) {
if (errorData?.code === 'PIX_REFUND_DEADLINE_EXCEEDED') {
throw new Error('The 89-day refund window has expired');
}
if (errorData?.code === 'PIX_REFUND_EXCEEDS_AVAILABLE') {
throw new Error('Refund amount exceeds the available amount for refund');
}
}
if (error.response?.status === 404) {
throw new Error('Original transaction not found');
}
throw new Error(errorData?.message || 'Error processing refund');
}
throw error;
}
}
// Usage - Full Refund
async function fullRefund(token: string, transactionId: string, originalValue: number) {
return await refundPixPayment(
token,
transactionId,
originalValue,
'Cancelamento total do pedido'
);
}
// Usage - Partial Refund
async function partialRefund(token: string, transactionId: string, itemValue: number) {
return await refundPixPayment(
token,
transactionId,
itemValue,
'Devolução de 1 item do pedido'
);
}
// Practical example
const token = 'your_token_here';
const transactionId = '7845';
// Refund R$ 75.00 from a R$ 150.00 transaction
refundPixPayment(token, transactionId, 75.00, 'Cliente solicitou devolução 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:
"""
Refund a received PIX payment
Args:
token: Valid Bearer token
original_transaction_id: Original transaction ID (Cash-In)
refund_amount: Amount to be refunded
reason: Refund reason (optional)
Returns:
Created refund data
"""
url = f'https://api.fypay.me/api/pix/refund-in/{original_transaction_id}'
payload = {
'refundValue': round(refund_amount, 2),
'reason': reason or 'Estorno solicitado pelo 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('PIX refund initiated successfully!')
print(f"Refund Transaction ID: {data['transactionId']}")
print(f"Original External ID: {data['externalId']}")
print(f"Refund Amount: R$ {data['refundValue']:.2f}")
print(f"Status: {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))
# Handle specific errors
if e.response.status_code == 400:
if error_data.get('code') == 'PIX_REFUND_DEADLINE_EXCEEDED':
raise Exception('The 89-day refund window has expired')
if error_data.get('code') == 'PIX_REFUND_EXCEEDS_AVAILABLE':
raise Exception('Refund amount exceeds the available amount for refund')
raise Exception(f'Invalid data: {error_message}')
if e.response.status_code == 404:
raise Exception('Original transaction not found')
raise Exception(f'Error processing refund: {error_message}')
# Usage
token = 'your_token_here'
transaction_id = '7845'
# Partial refund
refund = refund_pix_payment(
token=token,
original_transaction_id=transaction_id,
refund_amount=75.00,
reason='Cliente solicitou devolução de 1 item do pedido'
)
# Full refund
def full_refund(token: str, transaction_id: str, original_value: float):
"""Performs a full refund"""
return refund_pix_payment(
token=token,
original_transaction_id=transaction_id,
refund_amount=original_value,
reason='Cancelamento total do 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 ?? 'Estorno solicitado pelo 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('The 89-day refund window has expired');
}
if (($errorData['code'] ?? '') === 'PIX_REFUND_EXCEEDS_AVAILABLE') {
throw new Exception('Refund amount exceeds the available amount for refund');
}
}
if ($httpCode === 404) {
throw new Exception('Original transaction not found');
}
throw new Exception("Error processing refund: $errorMessage");
}
$data = json_decode($response, true);
echo "PIX refund initiated successfully!" . PHP_EOL;
echo "Refund Transaction ID: {$data['transactionId']}" . PHP_EOL;
echo "Original External ID: {$data['externalId']}" . PHP_EOL;
echo "Refund Amount: R$ " . number_format($data['refundValue'], 2, ',', '.') . PHP_EOL;
echo "Status: {$data['status']}" . PHP_EOL;
return $data;
}
// Usage
$token = 'your_token_here';
$transactionId = '7845';
// Partial refund
$refund = refundPixPayment(
$token,
$transactionId,
75.00,
'Cliente solicitou devolução de 1 item do pedido'
);Use Cases
1. E-commerce - Product Returns
class OrderRefundSystem {
constructor(private token: string) {}
async processItemReturn(orderId: string, returnedItems: OrderItem[]) {
// Fetch the original order transaction
const originalTransaction = await this.getTransactionByOrderId(orderId);
// Calculate total amount to refund
const refundAmount = returnedItems.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
// Check if it does not exceed the original transaction value
const availableForRefund = await this.getAvailableRefundAmount(
originalTransaction.id
);
if (refundAmount > availableForRefund) {
throw new Error(
`Requested amount (R$ ${refundAmount.toFixed(2)}) exceeds the available ` +
`refund amount (R$ ${availableForRefund.toFixed(2)})`
);
}
// Generate refund description
const itemsDescription = returnedItems
.map(item => `${item.name} (${item.quantity}x)`)
.join(', ');
// Process refund
const refund = await refundPixPayment(
this.token,
originalTransaction.id,
refundAmount,
`Devolução de itens: ${itemsDescription}`
);
// Update order status
await this.updateOrderStatus(orderId, 'PARTIALLY_REFUNDED', refund);
// Notify customer
await this.notifyCustomerRefund(orderId, refundAmount);
return refund;
}
async getAvailableRefundAmount(transactionId: string): Promise<number> {
// GET /api/pix/transaction/{id} already returns the refundable balance
const transaction = await this.getTransaction(transactionId);
return transaction.refundableAmount;
}
}
// Usage
interface OrderItem {
name: string;
price: number;
quantity: number;
}
const refundSystem = new OrderRefundSystem('your_token_here');
const returnedItems: OrderItem[] = [
{ name: 'Camiseta Azul', price: 49.90, quantity: 1 }
];
await refundSystem.processItemReturn('ORDER-12345', returnedItems);2. SaaS - Prorated Refund
from datetime import datetime, timedelta
from decimal import Decimal
class SubscriptionRefundManager:
"""Manages prorated subscription refunds"""
def __init__(self, token: str):
self.token = token
def calculate_prorated_refund(
self,
payment_date: datetime,
cancellation_date: datetime,
monthly_value: float
) -> float:
"""Calculates prorated refund based on unused days"""
# Calculate billing period days (30 days)
billing_period_days = 30
# Calculate days used
days_used = (cancellation_date - payment_date).days
# Calculate unused days
days_unused = billing_period_days - days_used
if days_unused <= 0:
return 0.0
# Calculate prorated amount
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:
"""Processes cancellation with prorated refund"""
# Fetch subscription data
subscription = self.get_subscription(subscription_id)
# Calculate prorated refund
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': 'No amount to refund'}
# Process refund
refund = refund_pix_payment(
token=self.token,
original_transaction_id=transaction_id,
refund_amount=refund_amount,
reason=f'Cancelamento de assinatura - Reembolso proporcional'
)
# Update subscription status
self.update_subscription_status(subscription_id, 'CANCELLED')
return refund
# Usage
manager = SubscriptionRefundManager('your_token_here')
# Customer paid R$ 99.00 on Jan 1st and cancelled on Jan 15th
# Prorated refund: 15 unused days
refund = manager.process_subscription_cancellation(
subscription_id='SUB-12345',
transaction_id='7845'
)3. Marketplace - Issue Compensation
class MarketplaceCompensation {
constructor(token) {
this.token = token;
}
async compensateForIssue(orderId, issueType) {
const order = await this.getOrder(orderId);
const compensationRules = this.getCompensationRules();
// Define compensation amount based on issue type
const compensationPercent = compensationRules[issueType] || 0;
const compensationAmount = order.value * (compensationPercent / 100);
if (compensationAmount === 0) {
throw new Error('Issue type not eligible for compensation');
}
// Perform partial refund as compensation
const refund = await refundPixPayment(
this.token,
order.transactionId,
compensationAmount,
`Compensação por ${issueType} - ${compensationPercent}% de desconto`
);
// Record compensation
await this.recordCompensation(orderId, issueType, compensationAmount);
return refund;
}
getCompensationRules() {
return {
'ATRASO_ENTREGA': 10, // 10% compensation
'PRODUTO_AVARIADO': 20, // 20% compensation
'ITEM_FALTANTE': 15, // 15% compensation
'QUALIDADE_INFERIOR': 25 // 25% compensation
};
}
}
// Usage
const compensation = new MarketplaceCompensation('your_token_here');
// Product arrived damaged - compensate with 20%
await compensation.compensateForIssue('ORDER-12345', 'PRODUTO_AVARIADO');Validations and Business Rules
Check Available Refund Amount
The original transaction's GET /api/pix/transaction/\{id\} already returns refundedAmount (sum of confirmed refunds), refundableAmount (how much can still be refunded), and relatedTransactions (the linked refunds):
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(
`Requested amount (R$ ${requestedAmount.toFixed(2)}) exceeds the available ` +
`refund amount (R$ ${transaction.refundableAmount.toFixed(2)}). ` +
`Total already refunded: R$ ${transaction.refundedAmount.toFixed(2)}`
);
}
return true;
}Check Refund Window
from datetime import datetime, timedelta
def can_refund_transaction(transaction_date: datetime) -> bool:
"""Checks if the transaction is still within the refund window"""
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'Refund window exceeded. '
f'Transaction made {days_passed} days ago. '
f'Maximum window: {max_refund_days} days.'
)
return True
# Usage
try:
can_refund_transaction(datetime(2024, 1, 1))
print('Transaction can be refunded')
except Exception as e:
print(f'Error: {e}')Refund Monitoring
The final refund result arrives through the CashInReversal webhook (refund_in event in the webhook configuration). Alternatively, query GET /api/pix/transaction/\{id\} with the refund's transactionId:
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('Refund confirmed!');
await this.onRefundConfirmed(refundTransactionId);
return true;
}
if (status === 'ERROR') {
await this.onRefundFailed(refundTransactionId);
throw new Error('Refund failed');
}
// Wait 3 seconds before checking again
await new Promise(resolve => setTimeout(resolve, 3000));
}
throw new Error('Timeout: Refund not confirmed within expected time');
}
async onRefundConfirmed(refundTransactionId: string) {
// Update database
// Notify customer
// Record log
}
async onRefundFailed(refundTransactionId: string) {
// Notify support team
// Record incident
// Create ticket for manual review
}
}Response Codes
| Code | Description | Meaning |
|---|---|---|
201 | Refund Created | PIX refund initiated successfully |
400 | Invalid Amount | PIX_REFUND_EXCEEDS_AVAILABLE (amount above what is available) or PIX_INVALID_REFUND_AMOUNT |
400 | Window Exceeded | PIX_REFUND_DEADLINE_EXCEEDED: 89-day refund window has expired |
400 | Non-refundable transaction | PIX_INVALID_TRANSACTION_TYPE_FOR_REFUND (not a receipt) or PIX_INVALID_TRANSACTION_STATE (receipt not yet confirmed) |
400 | Insufficient Balance | PIX_INSUFFICIENT_BALANCE: the account does not have balance to return the amount |
401 | Invalid Token | Token not provided, expired, or invalid |
404 | Transaction Not Found | PIX_TRANSACTION_NOT_FOUND: original transaction not found |
409 | Repeated externalId | PIX_DUPLICATE_EXTERNAL_ID: a refund with this externalId already exists |
409 | Refund Blocked | PIX_REFUND_BLOCKED_MISMATCH_HELD: transaction under value-mismatch review; the refund becomes available after the received amount is confirmed |
See the API Reference for full response field details.
Best Practices
Important Notes
Refunds cannot be cancelled once initiated. Make sure the amounts are correct before processing.
- Maximum window: 89 days after receipt
- Minimum amount: R$ 0.01
- Multiple refunds: Allowed, as long as the sum does not exceed the original amount