Fire Bankingdocs
Webhooks V2

REFUND

Descripción general

El webhook REFUND se envia cuando se procesa una devolución PIX. Hay dos escenarios:

  1. CashInReversal: Usted devolvio un PIX recibido (via /pix/:e2eid/devolucao/:id)
  2. CashOutReversal: Alguien devolvio un PIX que usted envío

Cuando se envia

  • Devolución de un PIX recibido confirmada (usted esta devolviendo)
  • Devolución de un PIX enviado recibida (alguien le esta devolviendo a usted)

Estructura del Payload

{
  "type": "REFUND",
  "data": {
    "id": 123,
    "txId": "7978c0c97ea847e78e8849634473c1f1",
    "pixKey": "7d9f0335-8dcc-4054-9bf9-0dbd61d36906",
    "status": "REFUNDED",
    "payment": {
      "amount": "100.00",
      "currency": "BRL"
    },
    "refunds": [
      {
        "status": "LIQUIDATED",
        "payment": {
          "amount": 50.00,
          "currency": "BRL"
        },
        "errorCode": null,
        "eventDate": "2024-01-15T10:30:00.000Z",
        "endToEndId": "D12345678901234567890123456789012",
        "information": "Devolucao solicitada pelo recebedor"
      }
    ],
    "createdAt": "2024-01-15T09:00:00.000Z",
    "errorCode": null,
    "endToEndId": "E12345678901234567890123456789012",
    "ticketData": {},
    "webhookType": "REFUND",
    "debtorAccount": {
      "ispb": null,
      "name": null,
      "issuer": null,
      "number": null,
      "document": null,
      "accountType": null
    },
    "idempotencyKey": "7978c0c97ea847e78e8849634473c1f1",
    "creditDebitType": "DEBIT",
    "creditorAccount": {
      "ispb": "18236120",
      "name": "NU PAGAMENTOS S.A.",
      "issuer": "260",
      "number": "12345-6",
      "document": "123.xxx.xxx-xx",
      "accountType": null
    },
    "localInstrument": "DICT",
    "transactionType": "PIX",
    "remittanceInformation": "Devolucao parcial"
  }
}

Diferencia entre CashInReversal y CashOutReversal

Usted devolvio un PIX recibido.

creditDebitType = DEBIT (saliendo de su cuenta)
debtorAccount = Su cuenta
creditorAccount = Quien recibira de vuelta

Ejemplo: Usted recibio R$ 100, luego devolvio R$ 50.

Alguien devolvio un PIX que usted envío.

creditDebitType = CREDIT (entrando a su cuenta)
debtorAccount = Quien esta devolviendo
creditorAccount = Su cuenta

Ejemplo: Usted envío R$ 100, el destinatario devolvio R$ 30.

Campos Importantes

typestring

Siempre "REFUND" para devoluciones.

data.idnumber

ID de la transacción original (no de la devolución).

data.statusstring

Estado de la transacción original después de la devolución:

  • REFUNDED: Devolución procesada
  • ERROR: Devolución fallida
data.paymentobject

Monto de la transacción original, no el de la devolución.

data.refundsarray

Lista de devoluciones realizadas. Contiene los detalles de cada devolución.

data.creditDebitTypestring

Dirección del dinero:

  • DEBIT: Saliendo de su cuenta (CashInReversal)
  • CREDIT: Entrando a su cuenta (CashOutReversal)
data.endToEndIdstring

E2E ID de la transacción original.

Procesamiento del Webhook

Ejemplo en Node.js

interface RefundWebhook {
  type: 'REFUND';
  data: {
    id: number;
    txId: string | null;
    status: 'REFUNDED' | 'ERROR';
    payment: {
      amount: string;
      currency: string;
    };
    refunds: Array<{
      status: 'LIQUIDATED' | 'ERROR';
      payment: {
        amount: number;  // number, no string!
        currency: string;
      };
      endToEndId: string;
      eventDate: string;
      information: string | null;
    }>;
    endToEndId: string;
    creditDebitType: 'CREDIT' | 'DEBIT';
  };
}

async function handleRefund(webhook: RefundWebhook) {
  const { data } = webhook;

  // Identificar tipo de devolución
  const isCashInReversal = data.creditDebitType === 'DEBIT';

  if (isCashInReversal) {
    // Usted devolvio un PIX recibido
    await handleCashInReversal(data);
  } else {
    // Alguien devolvio un PIX que usted envio
    await handleCashOutReversal(data);
  }
}

async function handleCashInReversal(data: RefundWebhook['data']) {
  // Buscar transacción original
  const original = await findTransactionByE2eId(data.endToEndId);

  // Procesar cada devolución
  for (const refund of data.refunds) {
    if (refund.status === 'LIQUIDATED') {
      // Devolución confirmada - debitar del saldo
      await processRefundOut({
        originalId: original.id,
        refundAmount: refund.payment.amount,  // ya es un numero
        refundE2eId: refund.endToEndId,
      });

      console.log(`Devuelto R$ ${refund.payment.amount} del PIX ${original.id}`);
    }
  }
}

async function handleCashOutReversal(data: RefundWebhook['data']) {
  // Buscar transferencia original
  const original = await findTransferByE2eId(data.endToEndId);

  // Procesar cada devolución recibida
  for (const refund of data.refunds) {
    if (refund.status === 'LIQUIDATED') {
      // Devolución recibida - acreditar al saldo
      await processRefundIn({
        originalId: original.id,
        refundAmount: refund.payment.amount,
        refundE2eId: refund.endToEndId,
      });

      console.log(`Recibido R$ ${refund.payment.amount} de devolución`);
    }
  }
}

Ejemplo en Python

from decimal import Decimal

def handle_refund(webhook: dict):
    data = webhook['data']

    # Identificar tipo
    is_cash_in_reversal = data['creditDebitType'] == 'DEBIT'

    if is_cash_in_reversal:
        handle_cash_in_reversal(data)
    else:
        handle_cash_out_reversal(data)


def handle_cash_in_reversal(data: dict):
    """Usted devolvio un PIX recibido"""
    original = find_transaction_by_e2e(data['endToEndId'])

    for refund in data['refunds']:
        if refund['status'] == 'LIQUIDATED':
            # Ya es un numero, convertir a Decimal
            amount = Decimal(str(refund['payment']['amount']))

            process_refund_out(
                original_id=original.id,
                refund_amount=amount,
                refund_e2e=refund['endToEndId']
            )


def handle_cash_out_reversal(data: dict):
    """Alguien devolvio un PIX que usted envio"""
    original = find_transfer_by_e2e(data['endToEndId'])

    for refund in data['refunds']:
        if refund['status'] == 'LIQUIDATED':
            amount = Decimal(str(refund['payment']['amount']))

            process_refund_in(
                original_id=original.id,
                refund_amount=amount,
                refund_e2e=refund['endToEndId']
            )

Devoluciones Parciales

Una transacción puede tener multiples devoluciones parciales. El array refunds contiene todas:

{
  "type": "REFUND",
  "data": {
    "payment": { "amount": "100.00" },
    "refunds": [
      {
        "payment": { "amount": 30.00 },
        "eventDate": "2024-01-15T10:00:00Z"
      },
      {
        "payment": { "amount": 50.00 },
        "eventDate": "2024-01-15T11:00:00Z"
      }
    ]
  }
}

Calculo del saldo de devolución:

const originalAmount = parseFloat(data.payment.amount);  // 100.00
const totalRefunded = data.refunds
  .filter(r => r.status === 'LIQUIDATED')
  .reduce((sum, r) => sum + r.payment.amount, 0);  // 80.00
const availableBalance = originalAmount - totalRefunded;  // 20.00

Nota: amount es number en refunds

Dentro del array refunds, el campo payment.amount es un number, no un string!

// data.payment.amount -> string "100.00"
// data.refunds[0].payment.amount -> number 50.00

// CORRECTO
const refundAmount = data.refunds[0].payment.amount;  // 50.00 (number)

// INCORRECTO - no se necesita parseFloat
const refundAmount = parseFloat(data.refunds[0].payment.amount);

Idempotencia

Use una combinacion de data.id y refunds[].endToEndId para idempotencia:

async function handleWebhook(webhook: RefundWebhook) {
  for (const refund of webhook.data.refunds) {
    const key = `refund:${webhook.data.id}:${refund.endToEndId}`;

    const isProcessed = await redis.sismember('processed', key);
    if (isProcessed) {
      continue;  // Ya procesado
    }

    await redis.sadd('processed', key);
    await processRefund(webhook.data, refund);
  }
}

Manejo de Errores

Si refund.status === 'ERROR', la devolución fallo:

for (const refund of data.refunds) {
  if (refund.status === 'ERROR') {
    console.error(`Devolución fallida: ${refund.errorCode}`);

    // Notificar sobre la falla
    await notifyRefundFailed({
      originalE2eId: data.endToEndId,
      refundE2eId: refund.endToEndId,
      errorCode: refund.errorCode,
    });
  }
}

Mejores Prácticas

Próximos Pasos

En esta página