REFUND
Visión General
El webhook REFUND se envía cuando una devolución PIX cambia de estado. Existen dos escenarios, cada uno ligado a un evento registrado en el panel:
- Evento
refund_in(CashInReversal): usted devolvió un PIX recibido (por ejemplo, víaPUT /api/pix/:e2eid/devolucao/:id) - Evento
refund_out(CashOutReversal): el destinatario devolvió un PIX que usted envió
Las dos situaciones llegan con type: "REFUND". Para recibir en formato V2, registre las URLs de esos eventos con el formato PIX BACEN.
Estructura del Payload
{
"data": {
"id": 12345,
"txId": "7978c0c97ea847e78e8849634473c1f1",
"pixKey": "7d9f0335-8dcc-4054-9bf9-0dbd61d36906",
"status": "REFUNDED",
"payment": {
"amount": "100",
"currency": "BRL"
},
"refunds": [
{
"status": "LIQUIDATED",
"payment": {
"amount": 50,
"currency": "BRL"
},
"errorCode": null,
"eventDate": "2024-01-15T10:30:00.000Z",
"endToEndId": "D12345678202401151030abcdefghijk",
"information": "Devolución solicitada por el receptor"
}
],
"createdAt": "2024-01-15T09:00:00.000Z",
"errorCode": null,
"endToEndId": "E12345678202401150900abcdefghijk",
"providerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"ticketData": {},
"webhookType": "REFUND",
"debtorAccount": {
"ispb": null,
"name": null,
"bankName": null,
"issuer": null,
"number": null,
"document": null,
"accountType": null
},
"idempotencyKey": "7978c0c97ea847e78e8849634473c1f1",
"creditDebitType": "DEBIT",
"creditorAccount": {
"ispb": "12345678",
"name": "Carlos Oliveira",
"bankName": "Banco Exemplo S.A.",
"issuer": "000",
"number": "123456",
"document": "12345678909",
"accountType": null
},
"localInstrument": "DICT",
"transactionType": "PIX",
"remittanceInformation": "Devolución solicitada por el receptor"
},
"type": "REFUND"
}refund_in vs refund_out
Usted devolvió un PIX recibido.
creditDebitType = DEBIT (saliendo de su cuenta)
debtorAccount = Su cuenta (campos null)
creditorAccount = Quien recibe de vueltaEjemplo: usted recibió R$ 100 y devolvió R$ 50.
El destinatario devolvió un PIX que usted envió.
creditDebitType = CREDIT (entrando en su cuenta)
debtorAccount = Quien está devolviendo
creditorAccount = Su cuenta (campos null)Ejemplo: usted envió R$ 100 y el destinatario devolvió R$ 30.
Campos Importantes
typestringSiempre "REFUND".
data.idnumberID de la transacción original (no de la devolución).
data.txIdstringIdentificador externo de la transacción original: el txid del cobro, o el x-idempotency-key de la transferencia original. El mismo valor viene en data.idempotencyKey.
data.statusstringEstado de la devolución que generó el webhook:
REFUNDED: devolución concluidaERROR: falla en la devoluciónPENDING: devolución en procesamiento
data.paymentobjectValor de la transacción original, no de la devolución.
data.refundsarrayContiene un único ítem: la devolución que generó este webhook. Cada devolución parcial llega en su propio webhook.
data.providerIdstringID de la devolución en el proveedor de liquidación.
data.creditDebitTypestringDirección del dinero:
DEBIT: saliendo de su cuenta (refund_in)CREDIT: entrando en su cuenta (refund_out)
data.endToEndIdstringEnd to End ID de la transacción original.
data.createdAtstringFecha de creación de la transacción original.
Procesando el Webhook
Ejemplo Node.js
interface RefundWebhook {
type: 'REFUND';
data: {
id: number;
txId: string | null;
status: 'PENDING' | 'REFUNDED' | 'ERROR';
payment: {
amount: string;
currency: 'BRL';
};
refunds: Array<{
status: 'PENDING' | 'LIQUIDATED' | 'ERROR';
payment: {
amount: number;
currency: 'BRL';
};
errorCode: string | null;
endToEndId: string | null;
eventDate: string;
information: string | null;
}>;
endToEndId: string | null;
providerId: string | null;
creditDebitType: 'CREDIT' | 'DEBIT';
};
}
async function handleRefund(webhook: RefundWebhook) {
const { data } = webhook;
const refund = data.refunds[0];
if (data.creditDebitType === 'DEBIT') {
await handleRefundIn(data, refund);
} else {
await handleRefundOut(data, refund);
}
}
async function handleRefundIn(data: RefundWebhook['data'], refund: RefundWebhook['data']['refunds'][number]) {
const original = await findReceiptByE2eId(data.endToEndId);
if (data.status === 'REFUNDED') {
await registerRefundSent({
originalId: original.id,
amount: refund.payment.amount,
refundE2eId: refund.endToEndId,
});
}
}
async function handleRefundOut(data: RefundWebhook['data'], refund: RefundWebhook['data']['refunds'][number]) {
const original = await findTransferByIdempotencyKey(data.txId);
if (data.status === 'REFUNDED') {
await registerRefundReceived({
originalId: original.id,
amount: refund.payment.amount,
refundE2eId: refund.endToEndId,
});
}
}Ejemplo Python
from decimal import Decimal
def handle_refund(webhook: dict):
data = webhook['data']
refund = data['refunds'][0]
if data['status'] != 'REFUNDED':
return
amount = Decimal(str(refund['payment']['amount']))
if data['creditDebitType'] == 'DEBIT':
original = find_receipt_by_e2e(data['endToEndId'])
register_refund_sent(original.id, amount, refund['endToEndId'])
else:
original = find_transfer_by_idempotency_key(data['txId'])
register_refund_received(original.id, amount, refund['endToEndId'])Devoluciones Parciales
Una transacción puede tener varias devoluciones parciales. Cada una genera su propio webhook REFUND, con el mismo data.id y data.payment.amount (los de la transacción original) y un ítem diferente en refunds.
Para saber cuánto aún se puede devolver, sume en su sistema las devoluciones concluidas:
const valorOriginal = Number(data.payment.amount);
const totalDevuelto = await sumCompletedRefunds(data.id);
const saldoDisponible = valorOriginal - totalDevuelto;Atención: amount es number en refunds
Dentro del array refunds, el campo payment.amount es number, no string.
const originalAmount = data.payment.amount;
const refundAmount = data.refunds[0].payment.amount;originalAmount es un string (ej.: "100") y refundAmount es un número (ej.: 50).
Idempotencia
Como data.id es el de la transacción original, se repite entre devoluciones parciales. Deduplique por la devolución:
async function handleWebhook(webhook: RefundWebhook) {
const refund = webhook.data.refunds[0];
const refundKey = refund.endToEndId ?? webhook.data.providerId;
const key = `refund:${webhook.data.id}:${refundKey}:${webhook.data.status}`;
const isNew = await redis.set(`webhook:${key}`, '1', 'NX', 'EX', 60 * 60 * 24 * 7);
if (!isNew) return;
await handleRefund(webhook);
}Manejo de Errores
Si data.status es ERROR, la devolución falló. El código queda en data.errorCode y en refunds[0].errorCode:
if (data.status === 'ERROR') {
await notifyRefundFailed({
originalE2eId: data.endToEndId,
errorCode: data.errorCode,
});
}