Safira Paydocs
Guías de Integración

PIX Refund-In (Devolución)

Descripción General

El endpoint de PIX Refund-In le permite revertir (devolver) pagos PIX recibidos a través de cobros generados vía Cash-In. Las devoluciones pueden ser totales o parciales y deben solicitarse dentro de 89 días desde la recepción.

Este endpoint requiere un Bearer token válido. Consulte la documentación de autenticación para más detalles.

Características

  • Devoluciones totales o parciales
  • Múltiples devoluciones parciales para la misma transacción
  • Ventana de hasta 89 días
  • Procesamiento instantáneo
  • Seguimiento por motivo de devolución

Cuándo Usar Devoluciones

Devolución Total

Retorna el 100% del monto recibido al pagador original.

Casos de uso:

  • Cancelación completa del pedido
  • Producto no enviado
  • Pago duplicado
  • Monto de cobro incorrecto

Devolución Parcial

Retorna solo una parte del monto recibido.

Casos de uso:

  • Devolución de artículos específicos
  • Compensación por problemas con producto/servicio
  • Ajustes de valor
  • 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/json

Parámetros de Ruta

idstringobrigatorio

ID de la transacción original (Cash-In) a devolver.

Ejemplo: "7845"

Cuerpo de la Solicitud

{
  "refundValue": 75.00,
  "reason": "Cliente solicitou devolução de 1 item do pedido"
}

Solicitud

curl -X POST https://api.safirapay.com/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"
  }'

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

refundValuenumberobrigatorio

Monto a devolver en BRL (Reales brasileños). Debe tener como máximo 2 decimales.

Validaciones:

  • Debe ser mayor o igual a 0.01
  • No puede exceder el monto disponible para devolución
  • La suma de todas las devoluciones no puede exceder el monto original

Ejemplo: 75.00

reasonstring

Motivo de la devolución (opcional, pero recomendado).

Máximo: 255 caracteres

Ejemplo: "Cliente solicitou devolução de 1 item do pedido"

Recomendación: Siempre proporcione un motivo claro para fines de auditoría

externalIdstring

ID externo para identificación de la devolución (opcional).

En la API del BACEN, corresponde al parámetro 'id' de la URL.

Ejemplo: "D123456789"

Estructura de la Respuesta

transactionIdstringsempre presente

ID de la nueva transacción de devolución generada.

Ejemplo: "7846"

Nota: Este es un ID diferente al de la transacción original

externalIdstringsempre presente

ID externo de la transacción de devolución.

Ejemplo: "D123456789"

statusstringsempre presente

Estado actual de la transacción de devolución.

Valores posibles:

  • PENDING: Devolución en proceso
  • CONFIRMED: Devolución confirmada y completada
  • ERROR: Error de procesamiento

Ejemplo: "PENDING"

refundValuenumbersempre presente

Monto de la devolución en BRL.

Ejemplo: 75.00

providerTransactionIdstringsempre presente

ID de la transacción en el proveedor (utilizado para correlación con webhooks).

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

generateTimestringsempre presente

Fecha 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 || 'Estorno solicitado pelo cliente'
  };

  try {
    const response = await axios.post<RefundResponse>(
      `https://api.safirapay.com/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?.message?.includes('prazo excedido')) {
          throw new Error('The 89-day refund window has expired');
        }
        if (errorData?.message?.includes('valor inválido')) {
          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.safirapay.com/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 'prazo excedido' in error_message:
                raise Exception('The 89-day refund window has expired')
            if 'valor inválido' in error_message:
                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.safirapay.com/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 (stripos($errorMessage, 'prazo excedido') !== false) {
                throw new Exception('The 89-day refund window has expired');
            }
            if (stripos($errorMessage, 'valor inválido') !== false) {
                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'
);

Códigos de Respuesta

CódigoDescripciónSignificado
201Devolución CreadaDevolución PIX iniciada exitosamente
400Monto InválidoEl monto de la devolución excede el monto disponible
400Ventana ExcedidaLa ventana de 89 días para devolución ha expirado
401Token InválidoToken no proporcionado, expirado o inválido
404Transacción No EncontradaTransacción padre no encontrada

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 que los montos sean correctos antes de procesar.

  • Ventana máxima: 89 días después de la recepción
  • Monto mínimo: R$ 0.01
  • Múltiples devoluciones: Permitidas, siempre que la suma no exceda el monto original

Próximos Pasos

En esta página