REFUND
Visão Geral
O webhook REFUND é enviado quando uma devolução PIX é processada. Existem dois cenários:
- CashInReversal: Você devolveu um PIX recebido (via
/pix/:e2eid/devolucao/:id) - CashOutReversal: Alguem devolveu um PIX que você enviou
Quando é enviado
- Devolução de PIX recebido confirmada (você devolvendo)
- Devolução de PIX enviado recebida (alguém devolvendo para você)
Estrutura do 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": "Devolução 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": "Devolução parcial"
}
}Diferença entre CashInReversal e CashOutReversal
Você devolveu um PIX recebido.
creditDebitType = DEBIT (saindo da sua conta)
debtorAccount = Sua conta
creditorAccount = Quem vai receber de voltaExemplo: Você recebeu R$ 100, depois devolveu R$ 50.
Alguem devolveu um PIX que você enviou.
creditDebitType = CREDIT (entrando na sua conta)
debtorAccount = Quem está devolvendo
creditorAccount = Sua contaExemplo: Você enviou R$ 100, o destinatário devolveu R$ 30.
Campos Importantes
typestringSempre "REFUND" para devoluções.
data.idnumberID da transação original (não da devolução).
data.statusstringStatus da transação original após a devolução:
REFUNDED: Devolução processadaERROR: Falha na devolução
data.paymentobjectValor da transação original, não da devolução.
data.refundsarrayLista de devoluções realizadas. Contém detalhes de cada devolução.
data.creditDebitTypestringDireção do dinheiro:
DEBIT: Saindo da sua conta (CashInReversal)CREDIT: Entrando na sua conta (CashOutReversal)
data.endToEndIdstringE2E ID da transação original.
Processando o Webhook
Exemplo 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, não 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 devolução
const isCashInReversal = data.creditDebitType === 'DEBIT';
if (isCashInReversal) {
// Você devolveu um PIX recebido
await handleCashInReversal(data);
} else {
// Alguem devolveu um PIX que você enviou
await handleCashOutReversal(data);
}
}
async function handleCashInReversal(data: RefundWebhook['data']) {
// Encontrar transação original
const original = await findTransactionByE2eId(data.endToEndId);
// Processar cada devolução
for (const refund of data.refunds) {
if (refund.status === 'LIQUIDATED') {
// Devolução confirmada - debitar do saldo
await processRefundOut({
originalId: original.id,
refundAmount: refund.payment.amount, // já é number
refundE2eId: refund.endToEndId,
});
console.log(`Devolvido R$ ${refund.payment.amount} do PIX ${original.id}`);
}
}
}
async function handleCashOutReversal(data: RefundWebhook['data']) {
// Encontrar transferência original
const original = await findTransferByE2eId(data.endToEndId);
// Processar cada devolução recebida
for (const refund of data.refunds) {
if (refund.status === 'LIQUIDATED') {
// Devolução recebida - creditar no saldo
await processRefundIn({
originalId: original.id,
refundAmount: refund.payment.amount,
refundE2eId: refund.endToEndId,
});
console.log(`Recebido R$ ${refund.payment.amount} de devolução`);
}
}
}Exemplo 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):
"""Você devolveu um PIX recebido"""
original = find_transaction_by_e2e(data['endToEndId'])
for refund in data['refunds']:
if refund['status'] == 'LIQUIDATED':
# Já é number, converter para 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):
"""Alguem devolveu um PIX que você enviou"""
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']
)Devoluções Parciais
Uma transação pode ter múltiplas devoluções parciais. O array refunds contém 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"
}
]
}
}Cálculo do saldo de devolução:
const valorOriginal = parseFloat(data.payment.amount); // 100.00
const totalDevolvido = data.refunds
.filter(r => r.status === 'LIQUIDATED')
.reduce((sum, r) => sum + r.payment.amount, 0); // 80.00
const saldoDisponível = valorOriginal - totalDevolvido; // 20.00Atenção: amount é number em refunds
Dentro do array refunds, o campo payment.amount é number, não string!
// data.payment.amount -> string "100.00"
// data.refunds[0].payment.amount -> number 50.00
// CORRETO
const refundAmount = data.refunds[0].payment.amount; // 50.00 (number)
// ERRADO - não precisa de parseFloat
const refundAmount = parseFloat(data.refunds[0].payment.amount);Idempotência
Use uma combinação de data.id e refunds[].endToEndId para idempotência:
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; // Já processado
}
await redis.sadd('processed', key);
await processRefund(webhook.data, refund);
}
}Tratamento de Erros
Se refund.status === 'ERROR', a devolução falhou:
for (const refund of data.refunds) {
if (refund.status === 'ERROR') {
console.error(`Devolução falhou: ${refund.errorCode}`);
// Notificar sobre falha
await notifyRefundFailed({
originalE2eId: data.endToEndId,
refundE2eId: refund.endToEndId,
errorCode: refund.errorCode,
});
}
}