docs
Integration Guides

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 CashInReversal webhook
  • 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/json

Path Parameters

idstringobrigatorio

Identifier 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

refundValuenumberobrigatorio

Amount 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 (refundableAmount in GET /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

reasonstring

Refund 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

externalIdstring

External 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 presente

ID of the new refund transaction generated.

Example: "7846"

Note: This is a different ID from the original transaction

externalIdstringsempre presente

External ID of the refund transaction.

Example: "D123456789"

statusstringsempre presente

Current refund transaction status.

Possible values:

  • PENDING: Refund being processed
  • CONFIRMED: Refund confirmed and completed
  • ERROR: Processing error

Example: "PENDING"

refundValuenumbersempre presente

Refund amount in BRL.

Example: 75.00

providerTransactionIdstringsempre presente

Transaction ID at the provider (used for correlation with webhooks).

Example: "7ef4fc3f-a187-495e-857c-e84d70612761"

generateTimestringsempre presente

Refund 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

CodeDescriptionMeaning
201Refund CreatedPIX refund initiated successfully
400Invalid AmountPIX_REFUND_EXCEEDS_AVAILABLE (amount above what is available) or PIX_INVALID_REFUND_AMOUNT
400Window ExceededPIX_REFUND_DEADLINE_EXCEEDED: 89-day refund window has expired
400Non-refundable transactionPIX_INVALID_TRANSACTION_TYPE_FOR_REFUND (not a receipt) or PIX_INVALID_TRANSACTION_STATE (receipt not yet confirmed)
400Insufficient BalancePIX_INSUFFICIENT_BALANCE: the account does not have balance to return the amount
401Invalid TokenToken not provided, expired, or invalid
404Transaction Not FoundPIX_TRANSACTION_NOT_FOUND: original transaction not found
409Repeated externalIdPIX_DUPLICATE_EXTERNAL_ID: a refund with this externalId already exists
409Refund BlockedPIX_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

Next Steps

On this page